From 93a185d80fec4f8b87eacd91f8b57388526f7dca Mon Sep 17 00:00:00 2001 From: Jamiras Date: Mon, 13 Apr 2026 13:22:01 -0600 Subject: [PATCH 1/7] show separate folders for each achievement set if multiple exist --- Core | 2 +- Source/ViewModels/AssetViewModelBase.cs | 15 ++++ Source/ViewModels/GameViewModel.cs | 12 +-- ...AchievementSetFolderNavigationViewModel.cs | 15 ++++ .../Navigation/NavigationListViewModel.cs | 88 ++++++++++++++++--- .../Navigation/NavigationViewModelBase.cs | 2 +- Source/ViewModels/ViewerViewModelBase.cs | 10 +-- Tests/ViewModels/AssetViewModelBaseTests.cs | 2 +- .../NavigationListViewModelTests.cs | 82 +++++++++++++---- .../NavigationViewModelBaseTests.cs | 2 +- 10 files changed, 182 insertions(+), 48 deletions(-) create mode 100644 Source/ViewModels/Navigation/AchievementSetFolderNavigationViewModel.cs diff --git a/Core b/Core index fc794b4b..2f7ed7a4 160000 --- a/Core +++ b/Core @@ -1 +1 @@ -Subproject commit fc794b4bbe20e099702c93fb75f0b97c651ed819 +Subproject commit 2f7ed7a4b9d588bc6fae41aaf9a0f8b87a909451 diff --git a/Source/ViewModels/AssetViewModelBase.cs b/Source/ViewModels/AssetViewModelBase.cs index b48ad1a6..03401d77 100644 --- a/Source/ViewModels/AssetViewModelBase.cs +++ b/Source/ViewModels/AssetViewModelBase.cs @@ -145,6 +145,20 @@ public int Id protected set { SetValue(IdProperty, value); } } + public int OwnerSetId + { + get + { + if (Generated.Asset != null && Generated.Asset.OwnerSetId != 0) + return Generated.Asset.OwnerSetId; + if (Local.Asset != null && Local.Asset.OwnerSetId != 0) + return Local.Asset.OwnerSetId; + if (Published.Asset != null && Published.Asset.OwnerSetId != 0) + return Published.Asset.OwnerSetId; + return 0; + } + } + public static readonly ModelProperty PointsProperty = ModelProperty.Register(typeof(AssetViewModelBase), "Points", typeof(int), 0); public int Points { @@ -187,6 +201,7 @@ private void UpdateModified() { Triggers = Local.TriggerList; TriggerSource = "Local (Not Generated)"; + CompareState = GeneratedCompareState.NotGenerated; } } else if (IsModified(Local, true)) diff --git a/Source/ViewModels/GameViewModel.cs b/Source/ViewModels/GameViewModel.cs index 83c39da6..ffe9de4a 100644 --- a/Source/ViewModels/GameViewModel.cs +++ b/Source/ViewModels/GameViewModel.cs @@ -224,18 +224,8 @@ internal void PopulateEditorList(AchievementScriptInterpreter interpreter) GeneratedAchievementCount = 0; } - if (NavigationNodes == null || !NavigationNodes.Any()) - { - var navigationNodes = new List(); - navigationNodes.Add(new ScriptFolderNavigationViewModel()); - navigationNodes.Add(new RichPresenceNavigationViewModel(null)); - navigationNodes.Add(new AssetFolderNavigationViewModel("Achievements")); - navigationNodes.Add(new AssetFolderNavigationViewModel("Leaderboards")); - NavigationNodes = navigationNodes; - } - var navigation = new NavigationListViewModel(this, _publishedAssets, _localAssets, _editors); - navigation.Merge(interpreter); + NavigationNodes = navigation.Merge(interpreter); SelectedNavigationNode = FindEditorNavigationNode(NavigationNodes, SelectedEditor); } diff --git a/Source/ViewModels/Navigation/AchievementSetFolderNavigationViewModel.cs b/Source/ViewModels/Navigation/AchievementSetFolderNavigationViewModel.cs new file mode 100644 index 00000000..7d0144a1 --- /dev/null +++ b/Source/ViewModels/Navigation/AchievementSetFolderNavigationViewModel.cs @@ -0,0 +1,15 @@ +using RATools.Data; + +namespace RATools.ViewModels.Navigation +{ + internal class AchievementSetFolderNavigationViewModel : AssetFolderNavigationViewModel + { + public AchievementSetFolderNavigationViewModel(AchievementSet achievementSet) + : base(achievementSet.Title) + { + AchievementSet = achievementSet; + } + + public AchievementSet AchievementSet { get; private set; } + } +} diff --git a/Source/ViewModels/Navigation/NavigationListViewModel.cs b/Source/ViewModels/Navigation/NavigationListViewModel.cs index 636c85cb..49d2ee3e 100644 --- a/Source/ViewModels/Navigation/NavigationListViewModel.cs +++ b/Source/ViewModels/Navigation/NavigationListViewModel.cs @@ -31,14 +31,14 @@ public NavigationListViewModel(GameViewModel gameViewModel, PublishedAssets publ private readonly List _editors; private readonly IBackgroundWorkerService _backgroundWorkerService; - private void MergeScript() + private void MergeScript(IEnumerable navigationNodes) { if (_gameViewModel.Script != null) { if (!_editors.Contains(_gameViewModel.Script)) _editors.Add(_gameViewModel.Script); - var scriptFolder = _gameViewModel.NavigationNodes.OfType().First(); + var scriptFolder = navigationNodes.OfType().First(); var scriptNode = scriptFolder.Children.OfType().FirstOrDefault(); if (scriptNode == null) { @@ -349,21 +349,45 @@ private void MergeLocal() } } - private void UpdateNavigationNodes() + private void UpdateNavigationNodes(IEnumerable navigationNodes) { - MergeScript(); + MergeScript(navigationNodes); var richPresence = _editors.OfType().FirstOrDefault(); if (richPresence != null) { - var richPresenceNode = _gameViewModel.NavigationNodes.OfType().First(); + var richPresenceNode = navigationNodes.OfType().First(); richPresenceNode.Editor = richPresence; } - var achievementsFolder = _gameViewModel.NavigationNodes.OfType().First(n => n.Label == "Achievements"); + bool hasSubsets = false; + foreach (var achievementSetNode in navigationNodes.OfType()) + { + var achievementsFolder = achievementSetNode.Children.OfType().First(n => n.Label == "Achievements"); + var leaderboardsFolder = achievementSetNode.Children.OfType().First(n => n.Label == "Leaderboards"); + UpdateAchievementSetNodes(achievementsFolder, leaderboardsFolder, achievementSetNode.AchievementSet); + hasSubsets = true; + } + + if (!hasSubsets) + { + var achievementsFolder = navigationNodes.OfType().First(n => n.Label == "Achievements"); + var leaderboardsFolder = navigationNodes.OfType().First(n => n.Label == "Leaderboards"); + UpdateAchievementSetNodes(achievementsFolder, leaderboardsFolder, null); + } + } + + private void UpdateAchievementSetNodes(AssetFolderNavigationViewModel achievementsFolder, AssetFolderNavigationViewModel leaderboardsFolder, AchievementSet achievementSet) + { var achievementNodes = achievementsFolder.Children.OfType().ToList(); foreach (var achievement in _editors.OfType()) { + if (achievementSet != null && achievement.OwnerSetId != achievementSet.Id) + { + if (achievement.OwnerSetId != 0 || achievementSet.Type != AchievementSetType.Core) + continue; + } + if (achievement.Generated.Asset == null && achievement.Local.Asset == null && achievement.Published.Asset == null) { // nothing keeping this node around, let it get discarded @@ -387,7 +411,6 @@ private void UpdateNavigationNodes() foreach (var achievementNode in achievementNodes) achievementsFolder.Children.Remove(achievementNode); - var leaderboardsFolder = _gameViewModel.NavigationNodes.OfType().First(n => n.Label == "Leaderboards"); var leaderboardNodes = leaderboardsFolder.Children.OfType().ToList(); foreach (var leaderboard in _editors.OfType()) { @@ -459,8 +482,32 @@ private static void ApplySort(ObservableCollection node } } - public void Merge(AchievementScriptInterpreter interpreter) + public IEnumerable Merge(AchievementScriptInterpreter interpreter) { + var hasSubsets = _gameViewModel.PublishedSets.Count() > 1; + + var navigationNodes = _gameViewModel.NavigationNodes; + if (navigationNodes == null || !navigationNodes.Any()) + { + var newNavigationNodes = new List(); + newNavigationNodes.Add(new ScriptFolderNavigationViewModel()); + newNavigationNodes.Add(new RichPresenceNavigationViewModel(null)); + AddAchievementSetNodes(newNavigationNodes, hasSubsets); + navigationNodes = newNavigationNodes.ToArray(); + } + else + { + var achievementsNode = navigationNodes.OfType().FirstOrDefault(f => f.Label == "Achievements"); + bool hadSubsets = achievementsNode == null; + if (hasSubsets != hadSubsets) + { + var newNavigationNodes = new List(); + newNavigationNodes.AddRange(navigationNodes.Take(2)); + AddAchievementSetNodes(newNavigationNodes, hasSubsets); + navigationNodes = newNavigationNodes.ToArray(); + } + } + foreach (var editor in _editors.OfType()) editor.SortOrder = 0; @@ -480,11 +527,32 @@ public void Merge(AchievementScriptInterpreter interpreter) _backgroundWorkerService.InvokeOnUiThread(() => { - UpdateNavigationNodes(); + UpdateNavigationNodes(navigationNodes); - foreach (var node in _gameViewModel.NavigationNodes) + foreach (var node in navigationNodes) ApplySort(node.Children); }); + + return navigationNodes; + } + + private void AddAchievementSetNodes(List newNavigationNodes, bool hasSubsets) + { + if (!hasSubsets) + { + newNavigationNodes.Add(new AssetFolderNavigationViewModel("Achievements")); + newNavigationNodes.Add(new AssetFolderNavigationViewModel("Leaderboards")); + } + else + { + foreach (var achievementSet in _gameViewModel.PublishedSets) + { + var setFolderNode = new AchievementSetFolderNavigationViewModel(achievementSet); + setFolderNode.AddChild(new AssetFolderNavigationViewModel("Achievements")); + setFolderNode.AddChild(new AssetFolderNavigationViewModel("Leaderboards")); + newNavigationNodes.Add(setFolderNode); + } + } } } } diff --git a/Source/ViewModels/Navigation/NavigationViewModelBase.cs b/Source/ViewModels/Navigation/NavigationViewModelBase.cs index 35edd19c..9ca2d11b 100644 --- a/Source/ViewModels/Navigation/NavigationViewModelBase.cs +++ b/Source/ViewModels/Navigation/NavigationViewModelBase.cs @@ -77,7 +77,7 @@ protected virtual string GetModificationMessage(GeneratedCompareState state) case GeneratedCompareState.PublishedDiffers: // ◐ return "Generated asset differs from published"; case GeneratedCompareState.NotGenerated: // ◖ - return "Published asset is not generated"; + return "Local asset is not generated"; default: return null; } diff --git a/Source/ViewModels/ViewerViewModelBase.cs b/Source/ViewModels/ViewerViewModelBase.cs index 05b1a507..727b5f09 100644 --- a/Source/ViewModels/ViewerViewModelBase.cs +++ b/Source/ViewModels/ViewerViewModelBase.cs @@ -97,27 +97,27 @@ public enum GeneratedCompareState None = 0, /// - /// generated matches core and/or local. no icon + /// published by not generated or generated matches core and/or local. no icon /// Same, /// - /// ◖ not generated (core only). half circle icon + /// ◖ not generated (local only). half circle icon /// NotGenerated, /// - /// ○ generated but not stored (no core or no local). hollow circle icon + /// ○ generated but not stored (no published or no local). hollow circle icon /// GeneratedOnly, /// - /// ◐ generated differs from core (local may or may not exist). half filled circle icon + /// ◐ generated differs from published (local may or may not exist). half filled circle icon /// PublishedDiffers, /// - /// ● generated differs from local (core may or may not exist). fully filled circle icon + /// ● generated differs from local (published may or may not exist). fully filled circle icon /// LocalDiffers, } diff --git a/Tests/ViewModels/AssetViewModelBaseTests.cs b/Tests/ViewModels/AssetViewModelBaseTests.cs index f06f0f62..f8d3d4ce 100644 --- a/Tests/ViewModels/AssetViewModelBaseTests.cs +++ b/Tests/ViewModels/AssetViewModelBaseTests.cs @@ -186,7 +186,7 @@ public void TestRefreshLocalNotGenerated() Assert.That(vmAsset.IsTitleModified, Is.False); Assert.That(vmAsset.IsDescriptionModified, Is.False); Assert.That(vmAsset.BadgeName, Is.EqualTo("Badge")); - Assert.That(vmAsset.CompareState, Is.EqualTo(GeneratedCompareState.None)); + Assert.That(vmAsset.CompareState, Is.EqualTo(GeneratedCompareState.NotGenerated)); Assert.That(vmAsset.ModificationMessage, Is.EqualTo("Not generated")); Assert.That(vmAsset.IsGenerated, Is.False); Assert.That(vmAsset.CanUpdate, Is.False); diff --git a/Tests/ViewModels/Nagivation/NavigationListViewModelTests.cs b/Tests/ViewModels/Nagivation/NavigationListViewModelTests.cs index f23c734a..87a6c103 100644 --- a/Tests/ViewModels/Nagivation/NavigationListViewModelTests.cs +++ b/Tests/ViewModels/Nagivation/NavigationListViewModelTests.cs @@ -27,13 +27,6 @@ public NavigationListViewModelHarness() _editors = new List(); - var navigationNodes = new List(); - navigationNodes.Add(new ScriptFolderNavigationViewModel()); - navigationNodes.Add(new RichPresenceNavigationViewModel(null)); - navigationNodes.Add(new AssetFolderNavigationViewModel("Achievements")); - navigationNodes.Add(new AssetFolderNavigationViewModel("Leaderboards")); - NavigationNodes = navigationNodes; - ServiceRepository.Reset(); ServiceRepository.Instance.RegisterInstance(new Mock().Object); } @@ -42,20 +35,29 @@ public void Initialize() { if (Game == null) { - Game = new MockGameViewModel(_fileSystemService.Object); - Game.SetValue(GameViewModel.NavigationNodesProperty, NavigationNodes); - _publishedAssets = new PublishedAssets("1234.json", _fileSystemService.Object); _localAssets = new LocalAssets("1234-User.txt", _fileSystemService.Object); + + Game = new MockGameViewModel(_fileSystemService.Object, _publishedAssets, _localAssets); + Game.SetValue(GameViewModel.NavigationNodesProperty, NavigationNodes); } } + public void InitializeSubsets() + { + Initialize(); + + var sets = (List)_publishedAssets.Sets; + sets.Add(new AchievementSet { Id = 1111, Title = Game.Title, Type = AchievementSetType.Core }); + sets.Add(new AchievementSet { Id = 2222, Title = "Bonus", Type = AchievementSetType.Bonus }); + } + public void Merge(AchievementScriptInterpreter interpreter) { Initialize(); var viewModel = new NavigationListViewModel(Game, _publishedAssets, _localAssets, _editors, _backgroundWorkerService.Object); - viewModel.Merge(interpreter); + NavigationNodes = viewModel.Merge(interpreter).ToList(); } public Achievement CreateAchievement(string title, int points = 5, AchievementType type = AchievementType.None) @@ -105,10 +107,13 @@ public MockScriptViewModel() class MockGameViewModel : GameViewModel { - public MockGameViewModel(IFileSystemService fileSystemService) + public MockGameViewModel(IFileSystemService fileSystemService, PublishedAssets publishedAssets, LocalAssets localAssets) : base(1234, "Game Title", new Mock().Object, fileSystemService) { SetRACacheDirectory("C:\\RACache\\"); + + _publishedAssets = publishedAssets; + _localAssets = localAssets; } public void InitScript(string filename) @@ -170,8 +175,8 @@ public void TestMergeLocalAchievement() Assert.AreEqual("Test Achievement", achievementNode.Label); Assert.IsNotNull(achievementNode.Editor); Assert.AreSame(achievement, ((AchievementViewModel)achievementNode.Editor).Local.Asset); - Assert.AreEqual(GeneratedCompareState.None, achievementNode.CompareState); - Assert.IsNull(achievementNode.ModificationMessage); + Assert.AreEqual(GeneratedCompareState.NotGenerated, achievementNode.CompareState); + Assert.AreEqual("Local asset is not generated", achievementNode.ModificationMessage); Assert.IsNotNull(achievementNode.ContextMenu); Assert.AreEqual(1, achievementNode.ContextMenu.Count()); @@ -283,8 +288,8 @@ public void TestMergeLocalAndPublishedAchievementDifferentId() Assert.IsNotNull(achievementNode.Editor); Assert.IsNull(((AchievementViewModel)achievementNode.Editor).Published.Asset); Assert.AreSame(localAchievement, ((AchievementViewModel)achievementNode.Editor).Local.Asset); - Assert.AreEqual(GeneratedCompareState.None, achievementNode.CompareState); - Assert.IsNull(achievementNode.ModificationMessage); + Assert.AreEqual(GeneratedCompareState.NotGenerated, achievementNode.CompareState); + Assert.AreEqual("Local asset is not generated", achievementNode.ModificationMessage); achievementNode = harness.NavigationNodes[2].Children[1] as AchievementNavigationViewModel; Assert.IsNotNull(achievementNode); @@ -421,8 +426,8 @@ public void TestMergeLocalAndGeneratedAchievementDifferentId() Assert.IsNotNull(achievementNode.Editor); Assert.AreSame(localAchievement, ((AchievementViewModel)achievementNode.Editor).Local.Asset); Assert.IsNull(((AchievementViewModel)achievementNode.Editor).Generated.Asset); - Assert.AreEqual(GeneratedCompareState.None, achievementNode.CompareState); - Assert.IsNull(achievementNode.ModificationMessage); + Assert.AreEqual(GeneratedCompareState.NotGenerated, achievementNode.CompareState); + Assert.AreEqual("Local asset is not generated", achievementNode.ModificationMessage); Assert.IsNotNull(achievementNode.ContextMenu); Assert.AreEqual(1, achievementNode.ContextMenu.Count()); @@ -538,5 +543,46 @@ public void TestMergePublishedAndGeneratedAchievementDifferentId() Assert.AreEqual("Update Local", menuItem.Label); Assert.IsFalse(menuItem.Command.CanExecute(null)); } + + + [Test] + public void TestMergeSubsetGeneratedAchievement() + { + var harness = new NavigationListViewModelHarness(); + var achievement = harness.CreateAchievement("Test Achievement"); + achievement.OwnerSetId = 2222; + harness.InitializeSubsets(); + + var interpreter = new AchievementScriptInterpreter(); + interpreter.AddAchievement(achievement); + harness.Merge(interpreter); + + Assert.AreEqual("Game Title", harness.NavigationNodes[2].Label); + Assert.AreEqual(2, harness.NavigationNodes[2].Children?.Count ?? 0); + Assert.AreEqual("Achievements", harness.NavigationNodes[2].Children[0].Label); + Assert.AreEqual(0, harness.NavigationNodes[2].Children[0].Children?.Count ?? 0); + Assert.AreEqual("Leaderboards", harness.NavigationNodes[2].Children[1].Label); + Assert.AreEqual(0, harness.NavigationNodes[2].Children[1].Children?.Count ?? 0); + + Assert.AreEqual("Bonus", harness.NavigationNodes[3].Label); + Assert.AreEqual(2, harness.NavigationNodes[3].Children?.Count ?? 0); + + Assert.AreEqual("Achievements", harness.NavigationNodes[3].Children[0].Label); + Assert.AreEqual(1, harness.NavigationNodes[3].Children[0].Children?.Count ?? 0); + + var achievementNode = harness.NavigationNodes[3].Children[0].Children[0] as AchievementNavigationViewModel; + Assert.IsNotNull(achievementNode); + Assert.AreEqual("Test Achievement", achievementNode.Label); + Assert.IsNotNull(achievementNode.Editor); + Assert.AreSame(achievement, ((AchievementViewModel)achievementNode.Editor).Generated.Asset); + Assert.AreEqual(GeneratedCompareState.GeneratedOnly, achievementNode.CompareState); + Assert.AreEqual("Generated only", achievementNode.ModificationMessage); + + Assert.IsNotNull(achievementNode.ContextMenu); + Assert.AreEqual(1, achievementNode.ContextMenu.Count()); + var menuItem = achievementNode.ContextMenu.First(); + Assert.AreEqual("Update Local", menuItem.Label); + Assert.IsTrue(menuItem.Command.CanExecute(null)); + } } } diff --git a/Tests/ViewModels/Nagivation/NavigationViewModelBaseTests.cs b/Tests/ViewModels/Nagivation/NavigationViewModelBaseTests.cs index 709ab6fc..d53ae942 100644 --- a/Tests/ViewModels/Nagivation/NavigationViewModelBaseTests.cs +++ b/Tests/ViewModels/Nagivation/NavigationViewModelBaseTests.cs @@ -78,7 +78,7 @@ public void TestCompareStateAndModificationMessage() harness.SetCompareState(GeneratedCompareState.NotGenerated); Assert.That(harness.CompareState, Is.EqualTo(GeneratedCompareState.NotGenerated)); - Assert.That(harness.ModificationMessage, Is.EqualTo("Published asset is not generated")); + Assert.That(harness.ModificationMessage, Is.EqualTo("Local asset is not generated")); harness.SetCompareState(GeneratedCompareState.PublishedDiffers); Assert.That(harness.CompareState, Is.EqualTo(GeneratedCompareState.PublishedDiffers)); From cb3c3835526eea4d9432a06ce579a918d3a9513b Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 14 Apr 2026 06:19:05 -0600 Subject: [PATCH 2/7] detect subset count changing instead of just having subsets changing --- .../Navigation/NavigationListViewModel.cs | 63 +++++++++---------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/Source/ViewModels/Navigation/NavigationListViewModel.cs b/Source/ViewModels/Navigation/NavigationListViewModel.cs index 49d2ee3e..06a363c7 100644 --- a/Source/ViewModels/Navigation/NavigationListViewModel.cs +++ b/Source/ViewModels/Navigation/NavigationListViewModel.cs @@ -484,28 +484,40 @@ private static void ApplySort(ObservableCollection node public IEnumerable Merge(AchievementScriptInterpreter interpreter) { - var hasSubsets = _gameViewModel.PublishedSets.Count() > 1; - var navigationNodes = _gameViewModel.NavigationNodes; - if (navigationNodes == null || !navigationNodes.Any()) + var previousSubsetCount = navigationNodes != null ? navigationNodes.OfType().Count() : 1; + var subsetCount = _gameViewModel.PublishedSets.Count(); + + if (subsetCount != previousSubsetCount) { var newNavigationNodes = new List(); - newNavigationNodes.Add(new ScriptFolderNavigationViewModel()); - newNavigationNodes.Add(new RichPresenceNavigationViewModel(null)); - AddAchievementSetNodes(newNavigationNodes, hasSubsets); - navigationNodes = newNavigationNodes.ToArray(); - } - else - { - var achievementsNode = navigationNodes.OfType().FirstOrDefault(f => f.Label == "Achievements"); - bool hadSubsets = achievementsNode == null; - if (hasSubsets != hadSubsets) + if (navigationNodes != null && navigationNodes.Any()) { - var newNavigationNodes = new List(); newNavigationNodes.AddRange(navigationNodes.Take(2)); - AddAchievementSetNodes(newNavigationNodes, hasSubsets); - navigationNodes = newNavigationNodes.ToArray(); } + else + { + newNavigationNodes.Add(new ScriptFolderNavigationViewModel()); + newNavigationNodes.Add(new RichPresenceNavigationViewModel(null)); + } + + if (subsetCount < 2) + { + newNavigationNodes.Add(new AssetFolderNavigationViewModel("Achievements")); + newNavigationNodes.Add(new AssetFolderNavigationViewModel("Leaderboards")); + } + else + { + foreach (var achievementSet in _gameViewModel.PublishedSets) + { + var setFolderNode = new AchievementSetFolderNavigationViewModel(achievementSet); + setFolderNode.AddChild(new AssetFolderNavigationViewModel("Achievements")); + setFolderNode.AddChild(new AssetFolderNavigationViewModel("Leaderboards")); + newNavigationNodes.Add(setFolderNode); + } + } + + navigationNodes = newNavigationNodes.ToArray(); } foreach (var editor in _editors.OfType()) @@ -535,24 +547,5 @@ public IEnumerable Merge(AchievementScriptInterpreter i return navigationNodes; } - - private void AddAchievementSetNodes(List newNavigationNodes, bool hasSubsets) - { - if (!hasSubsets) - { - newNavigationNodes.Add(new AssetFolderNavigationViewModel("Achievements")); - newNavigationNodes.Add(new AssetFolderNavigationViewModel("Leaderboards")); - } - else - { - foreach (var achievementSet in _gameViewModel.PublishedSets) - { - var setFolderNode = new AchievementSetFolderNavigationViewModel(achievementSet); - setFolderNode.AddChild(new AssetFolderNavigationViewModel("Achievements")); - setFolderNode.AddChild(new AssetFolderNavigationViewModel("Leaderboards")); - newNavigationNodes.Add(setFolderNode); - } - } - } } } From 5e3bc1f8b848753735cf7dbd4a6a3798cc40650e Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 14 Apr 2026 06:24:40 -0600 Subject: [PATCH 3/7] assume one subset if none provided --- Source/ViewModels/Navigation/NavigationListViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/ViewModels/Navigation/NavigationListViewModel.cs b/Source/ViewModels/Navigation/NavigationListViewModel.cs index 06a363c7..dfc588b1 100644 --- a/Source/ViewModels/Navigation/NavigationListViewModel.cs +++ b/Source/ViewModels/Navigation/NavigationListViewModel.cs @@ -487,6 +487,8 @@ public IEnumerable Merge(AchievementScriptInterpreter i var navigationNodes = _gameViewModel.NavigationNodes; var previousSubsetCount = navigationNodes != null ? navigationNodes.OfType().Count() : 1; var subsetCount = _gameViewModel.PublishedSets.Count(); + if (subsetCount == 0) + subsetCount = 1; if (subsetCount != previousSubsetCount) { From f9edd37b32b5fcd00096945b4e86174b5834b269 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 14 Apr 2026 06:29:14 -0600 Subject: [PATCH 4/7] don't assume 1 subset for previous state if empty --- Source/ViewModels/Navigation/NavigationListViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ViewModels/Navigation/NavigationListViewModel.cs b/Source/ViewModels/Navigation/NavigationListViewModel.cs index dfc588b1..92ff6455 100644 --- a/Source/ViewModels/Navigation/NavigationListViewModel.cs +++ b/Source/ViewModels/Navigation/NavigationListViewModel.cs @@ -485,7 +485,7 @@ private static void ApplySort(ObservableCollection node public IEnumerable Merge(AchievementScriptInterpreter interpreter) { var navigationNodes = _gameViewModel.NavigationNodes; - var previousSubsetCount = navigationNodes != null ? navigationNodes.OfType().Count() : 1; + var previousSubsetCount = navigationNodes != null ? navigationNodes.OfType().Count() : 0; var subsetCount = _gameViewModel.PublishedSets.Count(); if (subsetCount == 0) subsetCount = 1; From f9d243e5c1053ee83501779d6378366cb82e9c6a Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 15 Apr 2026 12:37:58 -0600 Subject: [PATCH 5/7] fix property name --- Source/ViewModels/GameViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ViewModels/GameViewModel.cs b/Source/ViewModels/GameViewModel.cs index ffe9de4a..3230ece0 100644 --- a/Source/ViewModels/GameViewModel.cs +++ b/Source/ViewModels/GameViewModel.cs @@ -248,7 +248,7 @@ internal void UpdateCompileProgress(int progress, int line) } } - public static readonly ModelProperty NavigationNodesProperty = ModelProperty.Register(typeof(GameViewModel), "Editors", typeof(IEnumerable), new NavigationViewModelBase[0]); + public static readonly ModelProperty NavigationNodesProperty = ModelProperty.Register(typeof(GameViewModel), "NavigationNodes", typeof(IEnumerable), new NavigationViewModelBase[0]); public IEnumerable NavigationNodes { get { return (IEnumerable)GetValue(NavigationNodesProperty); } From a0cfb7b690faaefe31edc973866dc354388678f7 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 15 Apr 2026 13:32:40 -0600 Subject: [PATCH 6/7] support for local-only subsets --- Source/Parser/AchievementScriptInterpreter.cs | 11 ++++++++++ .../Parser/Internal/AssetExpressionGroup.cs | 8 ++++++++ Source/ViewModels/GameViewModel.cs | 8 +++++++- .../Navigation/NavigationListViewModel.cs | 20 +++++++++++++------ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/Source/Parser/AchievementScriptInterpreter.cs b/Source/Parser/AchievementScriptInterpreter.cs index 14146533..ad231bf2 100644 --- a/Source/Parser/AchievementScriptInterpreter.cs +++ b/Source/Parser/AchievementScriptInterpreter.cs @@ -24,6 +24,7 @@ public AchievementScriptInterpreter() _achievements = new Dictionary(); _leaderboards = new Dictionary(); _richPresence = new RichPresenceBuilder(); + Sets = Array.Empty(); _minimumVersion = RATools.Data.Version.MinimumVersion; } @@ -55,6 +56,11 @@ internal void AddAchievement(Achievement achievement) _achievements[achievement] = 0; } + /// + /// Gets the sets generated by the script. + /// + public IEnumerable Sets { get; private set; } + /// /// Gets the game identifier from the script. /// @@ -328,6 +334,7 @@ public bool Run(ExpressionGroupCollection expressionGroups, IScriptInterpreterCa _achievements.Clear(); _leaderboards.Clear(); _richPresence.Clear(); + var sets = new List(); foreach (var expressionGroup in expressionGroups.Groups.OfType()) { @@ -352,7 +359,11 @@ public bool Run(ExpressionGroupCollection expressionGroups, IScriptInterpreterCa result = false; } } + + if (expressionGroup.GeneratedSets != null) + sets.AddRange(expressionGroup.GeneratedSets); } + Sets = sets; SoftwareVersion minimumVersion = scriptContext.SerializationContext.MinimumVersion.OrNewer(_minimumVersion); uint maxAddress = 0; diff --git a/Source/Parser/Internal/AssetExpressionGroup.cs b/Source/Parser/Internal/AssetExpressionGroup.cs index ba130dab..3a59fc6f 100644 --- a/Source/Parser/Internal/AssetExpressionGroup.cs +++ b/Source/Parser/Internal/AssetExpressionGroup.cs @@ -16,6 +16,8 @@ public AssetExpressionGroup() public RichPresenceBuilder GeneratedRichPresence { get; private set; } + public AchievementSet[] GeneratedSets { get; private set; } + protected override ExpressionGroup CreateGroup() { return new AssetExpressionGroup(); @@ -48,6 +50,12 @@ internal void CaptureGeneratedAssets(AchievementScriptContext context) GeneratedRichPresence = context.RichPresence; context.RichPresence = null; } + + if (context.Sets.Count > 0) + { + GeneratedSets = context.Sets.ToArray(); + context.Sets.Clear(); + } } internal override void AdjustSourceLines(int adjustment) diff --git a/Source/ViewModels/GameViewModel.cs b/Source/ViewModels/GameViewModel.cs index 3230ece0..86eb3518 100644 --- a/Source/ViewModels/GameViewModel.cs +++ b/Source/ViewModels/GameViewModel.cs @@ -529,7 +529,13 @@ public string Title private set { SetValue(TitleProperty, value); } } - public IEnumerable PublishedSets { get { return _publishedAssets?.Sets ?? new AchievementSet[0]; } } + public IEnumerable PublishedSets + { + get + { + return _publishedAssets?.Sets ?? new [] { new AchievementSet { OwnerGameId = GameId, Title = Title } }; + } + } public static readonly ModelProperty GeneratedAchievementCountProperty = ModelProperty.Register(typeof(MainWindowViewModel), "GeneratedAchievementCount", typeof(int), 0); public int GeneratedAchievementCount diff --git a/Source/ViewModels/Navigation/NavigationListViewModel.cs b/Source/ViewModels/Navigation/NavigationListViewModel.cs index 92ff6455..014f2b19 100644 --- a/Source/ViewModels/Navigation/NavigationListViewModel.cs +++ b/Source/ViewModels/Navigation/NavigationListViewModel.cs @@ -485,12 +485,20 @@ private static void ApplySort(ObservableCollection node public IEnumerable Merge(AchievementScriptInterpreter interpreter) { var navigationNodes = _gameViewModel.NavigationNodes; + var previousSubsetCount = navigationNodes != null ? navigationNodes.OfType().Count() : 0; - var subsetCount = _gameViewModel.PublishedSets.Count(); - if (subsetCount == 0) - subsetCount = 1; + var sets = new List(); + if (interpreter != null) + sets.AddRange(interpreter.Sets); + foreach (var set in _gameViewModel.PublishedSets) + { + if (!sets.Any(s => s.Id == set.Id)) + sets.Add(set); + } + if (sets.Count == 0) + sets.Add(new AchievementSet { OwnerGameId = _gameViewModel.GameId, Title = _gameViewModel.Title }); - if (subsetCount != previousSubsetCount) + if (sets.Count != previousSubsetCount) { var newNavigationNodes = new List(); if (navigationNodes != null && navigationNodes.Any()) @@ -503,14 +511,14 @@ public IEnumerable Merge(AchievementScriptInterpreter i newNavigationNodes.Add(new RichPresenceNavigationViewModel(null)); } - if (subsetCount < 2) + if (sets.Count < 2) { newNavigationNodes.Add(new AssetFolderNavigationViewModel("Achievements")); newNavigationNodes.Add(new AssetFolderNavigationViewModel("Leaderboards")); } else { - foreach (var achievementSet in _gameViewModel.PublishedSets) + foreach (var achievementSet in sets) { var setFolderNode = new AchievementSetFolderNavigationViewModel(achievementSet); setFolderNode.AddChild(new AssetFolderNavigationViewModel("Achievements")); From dd15d1a3daa2ffd5c30dc60885325215fd9eb999 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 16 Apr 2026 09:30:50 -0600 Subject: [PATCH 7/7] add CHALLENGE subset type --- Source/Data/AchievementSetType.cs | 9 +++++++++ Source/Parser/Functions/AchievementSetFunction.cs | 1 + 2 files changed, 10 insertions(+) diff --git a/Source/Data/AchievementSetType.cs b/Source/Data/AchievementSetType.cs index 65552bf4..6c3129d2 100644 --- a/Source/Data/AchievementSetType.cs +++ b/Source/Data/AchievementSetType.cs @@ -23,6 +23,15 @@ public enum AchievementSetType /// Bonus, + /// + /// A unique way to play the game. + /// + /// + /// Allows loading the core set and potentially bonus sets. + /// Must be explicitly opted-in by the player. + /// + Challenge, + /// /// A unique way to play the game. /// diff --git a/Source/Parser/Functions/AchievementSetFunction.cs b/Source/Parser/Functions/AchievementSetFunction.cs index 9b003546..19df07e5 100644 --- a/Source/Parser/Functions/AchievementSetFunction.cs +++ b/Source/Parser/Functions/AchievementSetFunction.cs @@ -79,6 +79,7 @@ public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) case "BONUS": set.Type = AchievementSetType.Bonus; break; case "SPECIALTY": set.Type = AchievementSetType.Specialty; break; case "EXCLUSIVE": set.Type = AchievementSetType.Exclusive; break; + case "CHALLENGE": set.Type = AchievementSetType.Challenge; break; case "CORE": result = new ErrorExpression("Cannot add CORE set. Only one is allowed, and is provided by default.", type);