Skip to content

Commit

Permalink
Merge pull request #2008 from Wibble199/fix/gamestate-reflection-rewrite
Browse files Browse the repository at this point in the history
GameState Reflection Rewrite
  • Loading branch information
diogotr7 committed May 17, 2020
2 parents ae32ed1 + 64eea25 commit d9ed030
Show file tree
Hide file tree
Showing 115 changed files with 831 additions and 1,500 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<BooleanToVisibilityConverter x:Key="BoolToVisConv" />
<DataTemplate x:Key="ListItemTemplate">
<Grid>
<TextBlock Text="{Binding DisplayPath}" Padding="4,1" Margin="0,0,16,0" />
<TextBlock Text="{Binding DisplayName}" Padding="4,1" Margin="0,0,16,0" />
<Image Source="/Aurora;component/Resources/icons8-folder-30.png" Width="16" Height="16" HorizontalAlignment="Right" VerticalAlignment="Center" Visibility="{Binding IsFolder, Converter={StaticResource BoolToVisConv}}" />
</Grid>
</DataTemplate>
Expand All @@ -36,8 +36,8 @@

<!-- Up button and current "directory" -->
<StackPanel Grid.Row="0" Orientation="Vertical">
<Button Content="⬅ Previous" IsEnabled="{Binding WorkingPathStr, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type l:GameStateParameterPicker}}, Converter={StaticResource IsStringNotNullConv}}" Margin="6" Padding="6,2" Click="BackBtn_Click" />
<TextBlock Text="{Binding WorkingPathStr, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type l:GameStateParameterPicker}}}" Margin="6,0,6,6" VerticalAlignment="Center" />
<Button Content="⬅ Previous" IsEnabled="{Binding WorkingPath, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type l:GameStateParameterPicker}}, Converter={StaticResource IsStringNotNullConv}}" Margin="6" Padding="6,2" Click="BackBtn_Click" />
<TextBlock Text="{Binding WorkingPath, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type l:GameStateParameterPicker}}}" Margin="6,0,6,6" VerticalAlignment="Center" />
</StackPanel>

<!-- List boxes (aux is for animation) -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,52 +21,28 @@ public partial class GameStateParameterPicker : UserControl, INotifyPropertyChan
public event EventHandler<SelectedPathChangedEventArgs> SelectedPathChanged;
public event PropertyChangedEventHandler PropertyChanged;

private List<string> parameterList;

public GameStateParameterPicker() {
InitializeComponent();
}

#region UI Properties
/// <summary>
/// The current parts that make up the path. E.G. "LocalPCInfo/RAM" -> "LocalPCInfo", "RAM"
/// </summary>
private Stack<string> WorkingPath { get; set; } = new Stack<string>();

/// <summary>
/// Lazy-evaluated list of parameters for this application and property type.
/// </summary>
public List<string> ParameterList => parameterList ?? (parameterList = Application?.ParameterLookup?.GetParameters(PropertyType).ToList());
public string WorkingPath { get; set; } = "";

/// <summary>
/// Gets a list of items that should be displayed in the parameter list (based on the current "parent" variable).
/// </summary>
public IEnumerable<PathOption> CurrentParameterListItems {
public IEnumerable<GameStateParameterLookupEntry> CurrentParameterListItems {
get {
// If the application or param lookup is null, we don't know the parameters so do nothing
if (Application?.ParameterLookup == null) return null;

// If the given working path is a path to a variable (which it shouldn't be), pop the last item (the variable name) from the path to give just the "directory"
if (Application.ParameterLookup.IsValidParameter(WorkingPathStr))
WorkingPath.Pop();

// Generate the string version of this working path (and cache it)
var _workingPath = WorkingPathStr;
if (_workingPath != "") _workingPath += "/"; // If not at the root directory, add / to the end of the test path. This means it doesn't get confused with things such as `CPU` and `CPUUsage`.
return from path in ParameterList // With all properties in the current param lookup that are of a valid type (e.g. numbers)
where path.StartsWith(_workingPath) // Pick only the ones that start with the same working path
let pathSplit = path.Substring(_workingPath.Length).Split('/') // Get a list of all remaining parts of the path (e.g. if this was A/B/C and current path was A, pathSplit would be 'B', 'C')
let isFolder = pathSplit.Length > 1 // If there is more than one part of the path remaining, this must be a directory
group isFolder by pathSplit[0] into g // Group by the path name so duplicates are removed
orderby !g.First(), g.Key // Order the remaining (distinct) items by folders first, then order by their name
select new PathOption(g.Key, g.First()); // Finally, put them in a POCO so we can bind the UI to these properties.
if (Application.ParameterLookup.IsValidParameter(WorkingPath))
GoUp();

return Application.ParameterLookup.Children(WorkingPath, PropertyType).OrderBy(p => !p.IsFolder).ThenBy(p => p.DisplayName);
}
}

/// <summary>
/// Returns the string representation of the current working path.
/// </summary>
public string WorkingPathStr => string.Join("/", WorkingPath.Reverse());
#endregion

#region IsOpen Dependency Property
Expand Down Expand Up @@ -113,9 +89,9 @@ private static void SelectedPathDPChanged(DependencyObject sender, DependencyPro
} else {
// Else if an actual path has been given, split it up into it's ""directories""
// For the path to be valid (and to be passed as a param to this method) it will be a path to a variable, not a "directory". We use this assumption.
picker.WorkingPath = new Stack<string>(e.NewValue.ToString().Split('/'));
picker.WorkingPath.Pop(); // Remove the last one, since the working path should not include the actual var name
picker.NotifyChanged(nameof(WorkingPath), nameof(WorkingPathStr), nameof(ParameterList), nameof(CurrentParameterListItems)); // All these things will be different now, so trigger an update of anything requiring them
picker.WorkingPath = (string)e.NewValue;
picker.GoUp(); // Remove the last one, since the working path should not include the actual var name
picker.NotifyChanged(nameof(WorkingPath), nameof(CurrentParameterListItems)); // All these things will be different now, so trigger an update of anything requiring them
picker.mainListBox.SelectedValue = e.NewValue.ToString().Split('/').Last(); // The selected item in the list will be the last part of the path
}

Expand All @@ -141,18 +117,17 @@ public Application Application {
/// <summary>
/// The types of properties that will be shown to the user.
/// </summary>
public PropertyType PropertyType {
get => (PropertyType)GetValue(PropertyTypeProperty);
public GSIPropertyType PropertyType {
get => (GSIPropertyType)GetValue(PropertyTypeProperty);
set => SetValue(PropertyTypeProperty, value);
}

public static readonly DependencyProperty PropertyTypeProperty =
DependencyProperty.Register(nameof(PropertyType), typeof(PropertyType), typeof(GameStateParameterPicker), new PropertyMetadata(PropertyType.None, ApplicationOrPropertyTypeChange));
DependencyProperty.Register(nameof(PropertyType), typeof(GSIPropertyType), typeof(GameStateParameterPicker), new PropertyMetadata(GSIPropertyType.None, ApplicationOrPropertyTypeChange));

public static void ApplicationOrPropertyTypeChange(DependencyObject sender, DependencyPropertyChangedEventArgs e) {
var picker = (GameStateParameterPicker)sender;
picker.parameterList = null;
picker.NotifyChanged(nameof(ParameterList), nameof(CurrentParameterListItems));
picker.NotifyChanged(nameof(CurrentParameterListItems));

if (!picker.ValidatePath(picker.SelectedPath))
picker.SelectedPath = "";
Expand All @@ -165,11 +140,11 @@ public static void ApplicationOrPropertyTypeChange(DependencyObject sender, Depe
/// </summary>
private bool ValidatePath(string path) =>
// If application parameter context doesn't exist or there is no set type, assume non loaded and allow the path
Application?.ParameterLookup == null || PropertyType == PropertyType.None
Application?.ParameterLookup == null || PropertyType == GSIPropertyType.None
// An empty path is fine
|| string.IsNullOrEmpty(path)
// If we're in number mode, allow the selected path to be a double
|| (PropertyType == PropertyType.Number && double.TryParse(path, out var _))
|| (PropertyType == GSIPropertyType.Number && double.TryParse(path, out var _))
// If not in number mode, must be a valid path and have the same type as the expected property type
|| Application.ParameterLookup.IsValidParameter(path, PropertyType);

Expand Down Expand Up @@ -215,13 +190,13 @@ private Storyboard CreateStoryboard(int from, int to, UIElement target) {

#region Event Handlers
private void BackBtn_Click(object sender, RoutedEventArgs e) {
if (WorkingPath.Count > 0) {
if (!string.IsNullOrEmpty(WorkingPath)) {
// Make the aux list box take on the same items as the current one so that when animated (since the aux is moved to the middle first) it looks natural
auxillaryListbox.ItemsSource = CurrentParameterListItems;

Animate(-1);
WorkingPath.Pop(); // Remove the last "directory" off the working path
NotifyChanged(nameof(CurrentParameterListItems), nameof(WorkingPathStr)); // These properties will have changed so any UI stuff that relies on it should update
GoUp(); // Remove the last "directory" off the working path
NotifyChanged(nameof(CurrentParameterListItems), nameof(WorkingPath)); // These properties will have changed so any UI stuff that relies on it should update
}
}

Expand All @@ -237,29 +212,24 @@ private void MainListBox_PreviewMouseLeftButtonDown(object sender, System.Window
// Element selection code is adapted from http://kevin-berridge.blogspot.com/2008/06/wpf-listboxitem-double-click.html
var el = (UIElement)mainListBox.InputHitTest(e.GetPosition(mainListBox));
while (el != null && el != mainListBox) {
if (el is ListBoxItem item) {
if (el is ListBoxItem item && item.DataContext is GameStateParameterLookupEntry itemContext) {

// Since the user has picked an item on the list, we want to clear the numeric box so it is obvious to the user that the number is having no effect.
numericEntry.Value = null;

// Copy the current list items to the aux list box incase the list box is animated later. This must be done BEFORE the workingpath.push call.
// Copy the current list items to the aux list box incase the list box is animated later. This must be done BEFORE changing workingpath
auxillaryListbox.ItemsSource = CurrentParameterListItems;

// Add the clicked item to the working path (even if it is an end variable, not a "directory")
WorkingPath.Push(((PathOption)item.DataContext).Path);
if (itemContext.IsFolder) {
// If the user selected a directory, animate the box.
WorkingPath = itemContext.Path;
Animate(1);
NotifyChanged(nameof(CurrentParameterListItems), nameof(WorkingPath));

var path = string.Join("/", WorkingPath.Reverse());
if (Application?.ParameterLookup?.IsValidParameter(path) ?? false) {
// If it turns out the user has selected an end variable, we want to update the DependencyObject for the selected path
SelectedPath = path;
NotifyChanged(nameof(SelectedPath));
} else {
// If the user has selected a directory instead (i.e. isn't not a valid parameter) then perform the animation since there will now be new properties to choose from
Animate(1);
// Otherwise if the user selected a parameter, update the SelectedPath Dependency Property (which will fire a change event).
SelectedPath = itemContext.Path;
}

// Regardless of whether it was a variable or a directory, the list and path will have changed
NotifyChanged(nameof(CurrentParameterListItems), nameof(WorkingPathStr));
}
el = (UIElement)VisualTreeHelper.GetParent(el);
}
Expand All @@ -277,24 +247,15 @@ private void NumericEntry_ValueChanged(object sender, RoutedPropertyChangedEvent
}
#endregion

private void NotifyChanged(params string[] propNames) {
foreach (var prop in propNames)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}


/// <summary>
/// Basic POCO for holding a bit of metadata about a path option.
/// Removes the last parameter or folder from the working path.
/// </summary>
public class PathOption {
public PathOption(string path, bool isFolder) {
Path = path;
IsFolder = isFolder;
}
private void GoUp() =>
WorkingPath = WorkingPath.Contains("/") ? WorkingPath.Substring(0, WorkingPath.LastIndexOf("/")) : "";

public string DisplayPath => Path.CamelCaseToSpaceCase();
public string Path { get; }
public bool IsFolder { get; }
private void NotifyChanged(params string[] propNames) {
foreach (var prop in propNames)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}

Expand Down Expand Up @@ -325,10 +286,10 @@ public class IsStringNotNullOrWhitespaceConverter : IValueConverter {

/// <summary>
/// Converter that converts a PropertyType enum value to a GridLength. Used for binding onto one of the row definition properties to hide a row when
/// the property type is anything other than <see cref="PropertyType.Number" />.
/// the property type is anything other than <see cref="GSIPropertyType.Number" />.
/// </summary>
public class PropertyTypeToGridLengthConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new GridLength(0, (PropertyType)value == PropertyType.Number ? GridUnitType.Auto : GridUnitType.Pixel);
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new GridLength(0, (GSIPropertyType)value == GSIPropertyType.Number ? GridUnitType.Auto : GridUnitType.Pixel);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => new NotImplementedException();
}

Expand Down
2 changes: 1 addition & 1 deletion Project-Aurora/Project-Aurora/NetworkListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ private void ReceiveGameState(IAsyncResult result)
response.ContentLength64 = 0;
response.Close();
}
CurrentGameState = new GameState(JSON);
CurrentGameState = new EmptyGameState(JSON);
}

private void HandleNewIPCGameState(string gs_data)
Expand Down
8 changes: 4 additions & 4 deletions Project-Aurora/Project-Aurora/Profiles/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public class Application : ObjectSettings<ApplicationSettings>, IInit, ILightEve
public bool Disposed { get; protected set; } = false;
public ApplicationProfile Profile { get; set; }
public ObservableCollection<ApplicationProfile> Profiles { get; set; }
public Dictionary<string, Tuple<Type, Type>> ParameterLookup { get; set; } //Key = variable path, Value = {Return type, Parameter type}
public GameStateParameterLookup ParameterLookup { get; set; }
public bool HasLayers { get; set; }
public event EventHandler ProfileChanged;
public bool ScriptsLoaded { get; protected set; }
Expand Down Expand Up @@ -108,7 +108,7 @@ public Application(LightEventConfig config)
};
EffectScripts = new Dictionary<string, IEffectScript>();
if (config.GameStateType != null)
ParameterLookup = Utils.GameStateUtils.ReflectGameStateParameters(config.GameStateType);
ParameterLookup = new GameStateParameterLookup(config.GameStateType);
}

public virtual bool Initialize()
Expand Down Expand Up @@ -307,14 +307,14 @@ void InitialiseLayerCollection(ObservableCollection<Layer> collection) {
continue;
}

lyr.AnythingChanged += SaveProfilesEvent;
lyr.PropertyChanged += SaveProfilesEvent;
}

collection.CollectionChanged += (_, e) => {
if (e.NewItems != null)
foreach (Layer lyr in e.NewItems)
if (lyr != null)
lyr.AnythingChanged += SaveProfilesEvent;
lyr.PropertyChanged += SaveProfilesEvent;
SaveProfiles();
};
}
Expand Down
4 changes: 2 additions & 2 deletions Project-Aurora/Project-Aurora/Profiles/AutoJsonNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
namespace Aurora.Profiles {

/// <summary>
/// A version of <see cref="Node{TClass}"/> which automatically populates the fields defined on it from the parsed JSON data.
/// A version of <see cref="Node"/> which automatically populates the fields defined on it from the parsed JSON data.
/// </summary>
public class AutoJsonNode<TSelf> : Node<TSelf> where TSelf : AutoJsonNode<TSelf> {
public class AutoJsonNode<TSelf> : Node where TSelf : AutoJsonNode<TSelf> {
// Did consider implementing this auto feature as a Fody weaver however, should profiles become plugin-based, each plugin would need to use Fody if they
// wished to have the automatic capability. Doing it as a class that can be extended means that no additional setup is required for plugin authors.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Aurora.Profiles.Borderlands2.GSI
/// <summary>
/// A class representing various information relating to Borderlands 2
/// </summary>
public class GameState_Borderlands2 : GameState<GameState_Borderlands2>
public class GameState_Borderlands2 : GameState
{
private Player_Borderlands2 player;

Expand Down Expand Up @@ -38,13 +38,5 @@ public GameState_Borderlands2() : base()
public GameState_Borderlands2(string json_data) : base(json_data)
{
}

/// <summary>
/// A copy constructor, creates a GameState_Borderlands2 instance based on the data from the passed GameState instance.
/// </summary>
/// <param name="other_state">The passed GameState</param>
public GameState_Borderlands2(IGameState other_state) : base(other_state)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// Class representing player information
/// </summary>
public class Player_Borderlands2 : Node<Player_Borderlands2>
public class Player_Borderlands2 : Node
{
/// <summary>
/// Player's boost amount [0.0f, 1.0f]
Expand Down
Loading

0 comments on commit d9ed030

Please sign in to comment.