Skip to content

Commit

Permalink
Merge pull request #2297 from Nexus-Mods/loadorder-v2-TreeDataGrid-ba…
Browse files Browse the repository at this point in the history
…ckend

Load Order backend: populate properties from game code
  • Loading branch information
insomnious authored Nov 25, 2024
2 parents 46f55ec + 3a5cbc2 commit b079ec3
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel;
using NexusMods.Abstractions.Loadouts;

namespace NexusMods.Abstractions.Games;
Expand All @@ -17,10 +18,75 @@ public interface ISortableItemProviderFactory
/// <summary>
/// Returns id of the type of the loadout
/// </summary>
Guid StaticSortOrderTypeId { get; }
Guid SortOrderTypeId { get; }

/// <summary>
/// Display name for this sort order type
/// Display name for this sort order type
/// </summary>
string SortOrderName { get; }

/// <summary>
/// Short descriptive title for the load order, describing the override behavior of the sort order
/// </summary>
/// <example>
/// "Last Loaded plugin Wins"
/// </example>
/// <remarks>
/// Avoid using "higher" or "lower" terms, as the index numbers can be sorted both in ascending or descending order,
/// making their meaning ambiguous.
/// </remarks>
string OverrideInfoTitle { get; }

/// <summary>
/// Heading for more details load order override information
/// </summary>
/// <example>
/// "Load Order for REDmods in Cyberpunk 2077 - First Loaded Wins"
/// </example>
string OverrideInfoHeading { get; }

/// <summary>
/// Detailed description of the load order and its override behavior
/// </summary>
string OverrideInfoMessage { get; }

/// <summary>
/// Short tooltip message to explain the winning index number in the load order
/// </summary>
string WinnerIndexToolTip { get; }

/// <summary>
/// Header text for the index column
/// </summary>
string IndexColumnHeader { get; }

/// <summary>
/// Header text for the name column
/// </summary>
string NameColumnHeader { get; }

/// <summary>
/// Title text to display in case there are no sortable items to sort
/// </summary>
string EmptyStateMessageTitle { get; }

/// <summary>
/// Contents text to display in case there are no sortable items to sort
/// </summary>
string EmptyStateMessageContents { get; }

/// <summary>
/// Default direction (ascending/descending) in which sortIndexes should be sorted and displayed
/// </summary>
/// <remarks>
/// Usually ascending, but could be different depending on what the community prefers and is used to
/// </remarks>
ListSortDirection SortDirectionDefault { get; }

/// <summary>
/// Defines whether smaller or greater index numbers win in case of conflicts between items in sorting order
/// </summary>
IndexOverrideBehavior IndexOverrideBehavior { get; }
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace NexusMods.Abstractions.Games;

/// <summary>
/// Defines whether smaller or greater index numbers win in case of conflicts between items in sorting order
/// </summary>
public enum IndexOverrideBehavior
{
/// <summary>
/// Items with Smaller index numbers win in case of conflicts with greater index number items
/// </summary>
SmallerIndexWins,

/// <summary>
/// Items with Greater index numbers win in case of conflicts with smaller index number items
/// </summary>
GreaterIndexWins,
}
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ private async Task PersistSortableEntries(List<RedModSortableItem> orderList)
var newSortOrder = new Abstractions.Loadouts.SortOrder.New(ts)
{
LoadoutId = loadoutId,
SortOrderTypeId = parentFactory.StaticSortOrderTypeId,
SortOrderTypeId = parentFactory.SortOrderTypeId,
};

var newRedModSortOrder = new RedModSortOrder.New(ts, newSortOrder.SortOrderId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel;
using System.Diagnostics;
using DynamicData;
using NexusMods.Abstractions.Games;
Expand All @@ -11,9 +12,34 @@ public class RedModSortableItemProviderFactory : ISortableItemProviderFactory
{
private readonly IConnection _connection;
private readonly Dictionary<LoadoutId, RedModSortableItemProvider> _providers = new();
private static readonly Guid StaticTypeId = new("9120C6F5-E0DD-4AD2-A99E-836F56796950");

public Guid StaticSortOrderTypeId { get; } = new("9120C6F5-E0DD-4AD2-A99E-836F56796950");
public string SortOrderName { get; } = "REDmod Load Order";
public Guid SortOrderTypeId => StaticTypeId;

public string SortOrderName => "REDmod Load Order";

public string OverrideInfoTitle => "First Loaded REDmod Wins";

public string OverrideInfoHeading => "Load Order for REDmods in Cyberpunk 2077 - First Loaded Wins";

public string OverrideInfoMessage => """
Some Cyberpunk 2077 mods use REDmods modules to alter core gameplay elements. If two REDmods modify the same part of the game, the one loaded first will take priority and overwrite changes from those loaded later.
For example, the 1st position overwrites the 2nd, the 2nd overwrites the 3rd, and so on.
""";

public string WinnerIndexToolTip => "The REDmod that will overwrite all others";

public string IndexColumnHeader => "Load Order";

public string NameColumnHeader => "REDmod Name";

public string EmptyStateMessageTitle => "No REDmods detected";
public string EmptyStateMessageContents => "Some mods contain REDmods modules that can alter core gameplay elements. When detected they will appear here for load order configuration.";

public ListSortDirection SortDirectionDefault => ListSortDirection.Ascending;

public IndexOverrideBehavior IndexOverrideBehavior => IndexOverrideBehavior.SmallerIndexWins;

public RedModSortableItemProviderFactory(IConnection connection)
{
Expand Down Expand Up @@ -56,7 +82,7 @@ public RedModSortableItemProviderFactory(IConnection connection)
Debug.Assert(false, $"RedModSortableItemProviderFactory: provider not found for loadout {removal.Current.LoadoutId}");
continue;
}

// TODO: Delete SortOrder and SortableItem entities from DB if it isn't done in Synchronizer.DeleteLoadout()
provider.Dispose();
}
Expand All @@ -74,5 +100,4 @@ public ILoadoutSortableItemProvider GetLoadoutSortableItemProvider(LoadoutId loa

throw new InvalidOperationException($"RedModSortableItemProviderFactory: provider not found for loadout {loadoutId}");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,67 @@ namespace NexusMods.App.UI.Pages.Sorting;

public interface ILoadOrderViewModel : IViewModelInterface
{
/// <summary>
/// TreeDataGridAdapter for the Load Order, for setting up the TreeDataGrid
/// </summary>
LoadOrderTreeDataGridAdapter Adapter { get; }

/// <summary>
/// Name of this sort order type
/// </summary>
string SortOrderName { get; }

/// <summary>
/// The always visible First/Last wins heading text
/// Also used for trophy tooltip heading
/// </summary>
string InfoAlertTitle { get; }

/// <summary>
/// The title of the alert message, only visible if the alert is visible
/// </summary>
string InfoAlertHeading { get; }

/// <summary>
/// The contents of the alert message, only visible if the alert is visible
/// </summary>
string InfoAlertMessage { get; }

/// <summary>
/// Whether the alert message should be visible or not
/// </summary>
bool InfoAlertIsVisible { get; set; }

/// <summary>
/// Command to invoke when the info alert icon is pressed (either to show or hide the alert)
/// </summary>
ReactiveCommand<Unit, Unit> InfoAlertCommand { get; }

/// <summary>
/// Tooltip message contents for the trophy icon
/// </summary>
string TrophyToolTip { get; }

/// <summary>
/// The current ascending/descending direction in which the SortIndexes are sorted and displayed
/// </summary>
ListSortDirection SortDirectionCurrent { get; set; }

/// <summary>
/// Whether the winning item is at the top or bottom of the list
/// </summary>
/// <remarks>
/// Depends on the way the game load order works and can't be deduced exclusively from the sort direction
/// </remarks>
bool IsWinnerTop { get; }

/// <summary>
/// Title text for the empty state, in case there are no sortable items to display
/// </summary>
string EmptyStateMessageTitle { get; }

/// <summary>
/// Contents text for the empty state, in case there are no sortable items to display
/// </summary>
string EmptyStateMessageContents { get; }
}
50 changes: 32 additions & 18 deletions src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,38 @@ namespace NexusMods.App.UI.Pages.Sorting;
public class LoadOrderViewModel : AViewModel<ILoadOrderViewModel>, ILoadOrderViewModel
{
public string SortOrderName { get; }

// TODO: Populate these properly
public string InfoAlertTitle { get; } = "";
public string InfoAlertHeading { get; } = "";
public string InfoAlertMessage { get; } = "";
[Reactive] public bool InfoAlertIsVisible { get; set; } = false;
public string InfoAlertTitle { get; }
public string InfoAlertHeading { get; }
public string InfoAlertMessage { get; }
[Reactive] public bool InfoAlertIsVisible { get; set; }
public ReactiveCommand<Unit, Unit> InfoAlertCommand { get; } = ReactiveCommand.Create(() => { });
public string TrophyToolTip { get; } = "";
[Reactive] public ListSortDirection SortDirectionCurrent { get; set; } = ListSortDirection.Ascending;
[Reactive] public bool IsWinnerTop { get; set; } = true;
public string TrophyToolTip { get; }
[Reactive] public ListSortDirection SortDirectionCurrent { get; set; }
[Reactive] public bool IsWinnerTop { get; set; }
public string EmptyStateMessageTitle { get; }
public string EmptyStateMessageContents { get; }

public LoadOrderTreeDataGridAdapter Adapter { get; }

public LoadOrderViewModel(LoadoutId loadoutId, ISortableItemProviderFactory sortableItemProviderFactory)
public LoadOrderViewModel(LoadoutId loadoutId, ISortableItemProviderFactory itemProviderFactory)
{
SortOrderName = sortableItemProviderFactory.SortOrderName;
var provider = sortableItemProviderFactory.GetLoadoutSortableItemProvider(loadoutId);
var provider = itemProviderFactory.GetLoadoutSortableItemProvider(loadoutId);

SortOrderName = itemProviderFactory.SortOrderName;
InfoAlertTitle = itemProviderFactory.OverrideInfoTitle;
InfoAlertHeading = itemProviderFactory.OverrideInfoHeading;
InfoAlertMessage = itemProviderFactory.OverrideInfoMessage;
TrophyToolTip = itemProviderFactory.WinnerIndexToolTip;
EmptyStateMessageTitle = itemProviderFactory.EmptyStateMessageTitle;
EmptyStateMessageContents = itemProviderFactory.EmptyStateMessageContents;

// TODO: load these from settings
SortDirectionCurrent = itemProviderFactory.SortDirectionDefault;
IsWinnerTop = itemProviderFactory.IndexOverrideBehavior == IndexOverrideBehavior.SmallerIndexWins &&
SortDirectionCurrent == ListSortDirection.Ascending;
InfoAlertIsVisible = true;


Adapter = new LoadOrderTreeDataGridAdapter(provider);
Adapter.ViewHierarchical.Value = true;

Expand Down Expand Up @@ -69,22 +83,22 @@ protected override IColumn<ILoadOrderItemModel>[] CreateColumns(bool viewHierarc
[
// TODO: Use <see cref="ColumnCreator"/> to create the columns using interfaces
new HierarchicalExpanderColumn<ILoadOrderItemModel>(
inner: CreateIndexColumn(),
inner: CreateIndexColumn(_sortableItemsProvider.ParentFactory.IndexColumnHeader),
childSelector: static model => model.Children,
hasChildrenSelector: static model => model.HasChildren.Value,
isExpandedSelector: static model => model.IsExpanded
)
{
Tag = "expander",
},
CreateNameColumn(),
CreateNameColumn(_sortableItemsProvider.ParentFactory.NameColumnHeader),
];
}

private static IColumn<ILoadOrderItemModel> CreateIndexColumn()
private static IColumn<ILoadOrderItemModel> CreateIndexColumn(string headerName)
{
return new CustomTemplateColumn<ILoadOrderItemModel>(
header: "Load Order",
header: headerName,
cellTemplateResourceKey: "LoadOrderItemIndexColumnTemplate",
options: new TemplateColumnOptions<ILoadOrderItemModel>
{
Expand All @@ -97,10 +111,10 @@ private static IColumn<ILoadOrderItemModel> CreateIndexColumn()
};
}

private static IColumn<ILoadOrderItemModel> CreateNameColumn()
private static IColumn<ILoadOrderItemModel> CreateNameColumn(string headerName)
{
return new CustomTemplateColumn<ILoadOrderItemModel>(
header: "Name",
header: headerName,
cellTemplateResourceKey: "LoadOrderItemNameColumnTemplate",
options: new TemplateColumnOptions<ILoadOrderItemModel>
{
Expand Down

0 comments on commit b079ec3

Please sign in to comment.