Refactors Autoloot property loading.#520
Conversation
Moves Autoloot property loading and assembly loading logic to AutolootManager. This change centralizes the property management, and removes redundant loading logic from the ECV and Autoloot View Models.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughCentralises property and assembly initialisation into AutolootManager by adding LoadProperties and LoadAssemblies. Updates viewmodels to delegate loading to the manager, removes the Additional Assemblies UI and its code-behind, trims a DP case in a filter control, and updates license years. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as UI (ECV / Autoloot)
participant VM as ViewModel
participant MGR as AutolootManager
participant FS as FileSystem
participant ASM as ExternalAssembly
UI->>VM: Initialise
VM->>MGR: GetInstance()
VM->>MGR: LoadProperties(constraints)
MGR->>FS: Read Properties.json / Properties.Custom.json
FS-->>MGR: PropertyEntry[]
MGR->>MGR: Merge entries + AddSorted + register Layer/Skill_Bonus predicates
VM->>MGR: LoadAssemblies(constraints)
MGR->>ASM: Load configured assemblies (paths from options)
ASM-->>MGR: Assembly loaded
MGR->>ASM: Invoke public static Initialize(constraints) if present
ASM-->>MGR: Initialize invoked
MGR-->>VM: Constraints populated
VM-->>UI: Filters/properties ready
note over UI,VM: Removed Additional Assemblies UI and direct VM-based assembly loading
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧬 Code graph analysis (1)ClassicAssist/Data/Autoloot/AutolootManager.cs (6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull Request Overview
This PR centralizes Autoloot property loading and assembly loading logic by moving it from individual view models to the AutolootManager. The refactoring eliminates code duplication and consolidates property management in a single location.
- Removed duplicate property loading logic from ECV and Autoloot ViewModels
- Added LoadProperties and LoadAssemblies methods to AutolootManager
- Removed the AdditionalAssembliesControl UI components that were managing assemblies through the ECV interface
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| AdditionalAssembliesControl.xaml.cs | Completely removed - assembly management UI no longer needed |
| AdditionalAssembliesControl.xaml | Completely removed - assembly management UI no longer needed |
| EntityCollectionFilterViewModel.cs | Replaced local property loading logic with AutolootManager calls |
| EntityCollectionFilterControl.xaml.cs | Removed assembly collection handling code |
| EntityCollectionViewerSettingsWindow.xaml | Removed AdditionalAssembliesControl from UI |
| AutolootViewModel.cs | Replaced local property loading with AutolootManager calls and updated copyright |
| AutolootManager.cs | Added LoadProperties and LoadAssemblies methods to centralize loading logic |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| Assembly asm = Assembly.LoadFrom( fileName ); | ||
| IEnumerable<MethodInfo> initializeMethods = asm.GetTypes() | ||
| .Where( e => e.IsClass && e.IsPublic && e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, | ||
| new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) != null ).Select( e => | ||
| e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) ); | ||
|
|
||
| foreach ( MethodInfo initializeMethod in initializeMethods ) | ||
| { | ||
| // ReSharper disable once CoVariantArrayConversion | ||
| initializeMethod?.Invoke( null, new[] { constraints } ); |
There was a problem hiding this comment.
Loading assemblies from file paths without validation poses security risks. Consider adding file existence checks and exception handling around Assembly.LoadFrom().
| Assembly asm = Assembly.LoadFrom( fileName ); | |
| IEnumerable<MethodInfo> initializeMethods = asm.GetTypes() | |
| .Where( e => e.IsClass && e.IsPublic && e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, | |
| new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) != null ).Select( e => | |
| e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) ); | |
| foreach ( MethodInfo initializeMethod in initializeMethods ) | |
| { | |
| // ReSharper disable once CoVariantArrayConversion | |
| initializeMethod?.Invoke( null, new[] { constraints } ); | |
| if (!File.Exists(fileName)) | |
| { | |
| // Optionally log or handle missing file | |
| continue; | |
| } | |
| try | |
| { | |
| Assembly asm = Assembly.LoadFrom(fileName); | |
| IEnumerable<MethodInfo> initializeMethods = asm.GetTypes() | |
| .Where(e => e.IsClass && e.IsPublic && e.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static, null, | |
| new[] { typeof(ObservableCollection<PropertyEntry>) }, null) != null).Select(e => | |
| e.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(ObservableCollection<PropertyEntry>) }, null)); | |
| foreach (MethodInfo initializeMethod in initializeMethods) | |
| { | |
| // ReSharper disable once CoVariantArrayConversion | |
| initializeMethod?.Invoke(null, new[] { constraints }); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| // Optionally log the exception, e.g. Console.WriteLine or use a logging framework | |
| // Console.WriteLine($"Failed to load assembly '{fileName}': {ex}"); |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
ClassicAssist/Data/Autoloot/AutolootManager.cs (1)
160-174: Remove stray return; hoist local function for clarityThe unconditional return before the local LoadFile declaration is unnecessary and harms readability. Either remove the return or move LoadFile above its first use.
Apply this diff to remove the redundant return:
- return; - IEnumerable<PropertyEntry> LoadFile( string fileName ) { JsonSerializer serializer = new JsonSerializer();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
ClassicAssist/Data/Autoloot/AutolootManager.cs(2 hunks)ClassicAssist/UI/ViewModels/Agents/AutolootViewModel.cs(21 hunks)ClassicAssist/UI/Views/ECV/EntityCollectionViewerSettingsWindow.xaml(1 hunks)ClassicAssist/UI/Views/ECV/Filter/EntityCollectionFilterControl.xaml.cs(0 hunks)ClassicAssist/UI/Views/ECV/Filter/EntityCollectionFilterViewModel.cs(3 hunks)ClassicAssist/UI/Views/ECV/Settings/Controls/AdditionalAssembliesControl.xaml(0 hunks)ClassicAssist/UI/Views/ECV/Settings/Controls/AdditionalAssembliesControl.xaml.cs(0 hunks)
💤 Files with no reviewable changes (3)
- ClassicAssist/UI/Views/ECV/Settings/Controls/AdditionalAssembliesControl.xaml.cs
- ClassicAssist/UI/Views/ECV/Settings/Controls/AdditionalAssembliesControl.xaml
- ClassicAssist/UI/Views/ECV/Filter/EntityCollectionFilterControl.xaml.cs
🧰 Additional context used
🧬 Code graph analysis (3)
ClassicAssist/Data/Autoloot/AutolootManager.cs (5)
ClassicAssist/Data/Autoloot/PropertyEntry.cs (1)
PropertyEntry(8-78)ClassicAssist/Data/AssistantOptions.cs (1)
AssistantOptions(19-353)ClassicAssist.Shared/Misc/ExtensionMethods.cs (1)
AddSorted(35-50)ClassicAssist/UO/Data/TileData.cs (1)
TileData(12-161)ClassicAssist/UO/Objects/Property.cs (1)
Property(3-19)
ClassicAssist/UI/Views/ECV/Filter/EntityCollectionFilterViewModel.cs (1)
ClassicAssist/Data/Autoloot/AutolootManager.cs (4)
AutolootManager(36-175)AutolootManager(50-68)LoadProperties(88-174)LoadAssemblies(70-86)
ClassicAssist/UI/ViewModels/Agents/AutolootViewModel.cs (1)
ClassicAssist/Data/Autoloot/AutolootManager.cs (4)
AutolootManager(36-175)AutolootManager(50-68)LoadProperties(88-174)LoadAssemblies(70-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
ClassicAssist/UI/Views/ECV/EntityCollectionViewerSettingsWindow.xaml (2)
56-59: Approve the layout simplification.The removal of the second row definition and the margin adjustment to the Container Sets GroupBox are appropriate changes that align with the removal of the Additional Assemblies UI section. The reduced bottom margin (from 15 to 10) maintains visual consistency now that there's no second row below.
1-78: Additional Assemblies removed; confirm alternate management path
- No
AdditionalAssemblproperties or XAML bindings remain inEntityCollectionViewerSettingsViewModelor ECV views.- Ensure additional assemblies can still be managed via AutolootManager or another interface.
ClassicAssist/UI/Views/ECV/Filter/EntityCollectionFilterViewModel.cs (1)
76-78: Centralised initialisation via AutolootManager looks goodShifting property/assembly loading to AutolootManager simplifies this VM and reduces duplication. LGTM. Ensure the manager’s loading is exception‑safe as per the manager changes.
Also applies to: 120-122
| public void LoadAssemblies( ObservableCollection<PropertyEntry> constraints ) | ||
| { | ||
| foreach ( string fileName in AssistantOptions.Assemblies ) | ||
| { | ||
| Assembly asm = Assembly.LoadFrom( fileName ); | ||
| IEnumerable<MethodInfo> initializeMethods = asm.GetTypes() | ||
| .Where( e => e.IsClass && e.IsPublic && e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, | ||
| new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) != null ).Select( e => | ||
| e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) ); | ||
|
|
||
| foreach ( MethodInfo initializeMethod in initializeMethods ) | ||
| { | ||
| // ReSharper disable once CoVariantArrayConversion | ||
| initializeMethod?.Invoke( null, new[] { constraints } ); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Harden assembly loading and avoid duplicate loads
Current code can crash initialisation and double‑load assemblies:
- No exception handling for missing/corrupt assemblies or ReflectionTypeLoadException.
- AssistantOptions.Load uses Assembly.LoadFile earlier; using LoadFrom here can load the same DLL again into a different context.
Recommend:
- Reuse already loaded assemblies by Location; fall back to LoadFile to match prior usage.
- Guard with File.Exists and wrap in try/catch.
Apply this diff:
- public void LoadAssemblies( ObservableCollection<PropertyEntry> constraints )
- {
- foreach ( string fileName in AssistantOptions.Assemblies )
- {
- Assembly asm = Assembly.LoadFrom( fileName );
- IEnumerable<MethodInfo> initializeMethods = asm.GetTypes()
- .Where( e => e.IsClass && e.IsPublic && e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null,
- new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) != null ).Select( e =>
- e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) );
-
- foreach ( MethodInfo initializeMethod in initializeMethods )
- {
- // ReSharper disable once CoVariantArrayConversion
- initializeMethod?.Invoke( null, new[] { constraints } );
- }
- }
- }
+ public void LoadAssemblies( ObservableCollection<PropertyEntry> constraints )
+ {
+ foreach ( string fileName in AssistantOptions.Assemblies )
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(fileName) || !File.Exists(fileName))
+ {
+ continue;
+ }
+
+ // Prefer reusing an existing load; fall back to LoadFile to match AssistantOptions.Load
+ Assembly asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a =>
+ {
+ try { return string.Equals(a.Location, fileName, StringComparison.OrdinalIgnoreCase); }
+ catch { return false; }
+ }) ?? Assembly.LoadFile(fileName);
+
+ IEnumerable<MethodInfo> initializeMethods;
+ try
+ {
+ initializeMethods = asm.GetTypes()
+ .Where( e => e.IsClass && e.IsPublic && e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null,
+ new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) != null )
+ .Select( e => e.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) );
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ initializeMethods = ex.Types
+ .Where( t => t != null && t.IsClass && t.IsPublic && t.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null,
+ new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) != null )
+ .Select( t => t.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof( ObservableCollection<PropertyEntry> ) }, null ) );
+ }
+
+ foreach ( MethodInfo initializeMethod in initializeMethods )
+ {
+ // ReSharper disable once CoVariantArrayConversion
+ initializeMethod?.Invoke( null, new[] { constraints } );
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+ }| if (entry.Operator != AutolootOperator.NotPresent) | ||
| { | ||
| return properties | ||
| .Where(property => property.Arguments != null && property.Arguments.Length >= 1 && | ||
| (property.Arguments[0].Equals(entry.Additional, StringComparison.CurrentCultureIgnoreCase) || | ||
| string.IsNullOrEmpty(entry.Additional))).Any(property => | ||
| AutolootHelpers.Operation(entry.Operator, Convert.ToInt32(property.Arguments[1]), entry.Value)); | ||
| } |
There was a problem hiding this comment.
Bounds bug in Skill_Bonus predicate (IndexOutOfRange risk)
Arguments length is checked for >= 1 but index 1 is accessed. This can throw when only one argument is present. Also, parsing should be guarded.
Apply this diff:
- if (entry.Operator != AutolootOperator.NotPresent)
- {
- return properties
- .Where(property => property.Arguments != null && property.Arguments.Length >= 1 &&
- (property.Arguments[0].Equals(entry.Additional, StringComparison.CurrentCultureIgnoreCase) ||
- string.IsNullOrEmpty(entry.Additional))).Any(property =>
- AutolootHelpers.Operation(entry.Operator, Convert.ToInt32(property.Arguments[1]), entry.Value));
- }
+ if (entry.Operator != AutolootOperator.NotPresent)
+ {
+ return properties
+ .Where(property => property.Arguments != null && property.Arguments.Length >= 2 &&
+ (property.Arguments[0].Equals(entry.Additional, StringComparison.CurrentCultureIgnoreCase) ||
+ string.IsNullOrEmpty(entry.Additional)))
+ .Any(property =>
+ int.TryParse(property.Arguments[1], out var val) &&
+ AutolootHelpers.Operation(entry.Operator, val, entry.Value));
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (entry.Operator != AutolootOperator.NotPresent) | |
| { | |
| return properties | |
| .Where(property => property.Arguments != null && property.Arguments.Length >= 1 && | |
| (property.Arguments[0].Equals(entry.Additional, StringComparison.CurrentCultureIgnoreCase) || | |
| string.IsNullOrEmpty(entry.Additional))).Any(property => | |
| AutolootHelpers.Operation(entry.Operator, Convert.ToInt32(property.Arguments[1]), entry.Value)); | |
| } | |
| if (entry.Operator != AutolootOperator.NotPresent) | |
| { | |
| return properties | |
| .Where(property => property.Arguments != null && property.Arguments.Length >= 2 && | |
| (property.Arguments[0].Equals(entry.Additional, StringComparison.CurrentCultureIgnoreCase) || | |
| string.IsNullOrEmpty(entry.Additional))) | |
| .Any(property => | |
| int.TryParse(property.Arguments[1], out var val) && | |
| AutolootHelpers.Operation(entry.Operator, val, entry.Value)); | |
| } |
🤖 Prompt for AI Agents
In ClassicAssist/Data/Autoloot/AutolootManager.cs around lines 141-148, the
predicate checks Arguments.Length >= 1 but then accesses Arguments[1], causing
an IndexOutOfRangeException and unguarded parsing; change the length check to >=
2, ensure you only evaluate the second argument when present, and use
int.TryParse to safely parse property.Arguments[1] (skip this property if
parsing fails) while preserving the existing Additional matching logic.
| _manager = AutolootManager.GetInstance(); | ||
| _manager.LoadProperties( Constraints ); | ||
| _manager.LoadAssemblies( Constraints ); | ||
| _manager.GetEntries = () => _items.ToList(); | ||
| _manager.CheckContainer = OnCorpseEvent; | ||
| _manager.CheckItems = CheckItems; | ||
| _manager.IsRunning = () => IsRunning; | ||
| _manager.MatchTextValue = () => MatchTextValue; | ||
| _manager.IsEnabled = () => Enabled; | ||
| _manager.SetEnabled = val => Enabled = val; |
There was a problem hiding this comment.
Singleton delegates capture this — leaks the ViewModel
Assigning instance-bound delegates to the long‑lived AutolootManager roots AutolootViewModel and prevents GC after the view closes. Provide a detach path or use weak references.
Minimal fix option:
- Expose a DetachFromManager() that sets all manager delegates back to no‑ops or null and call it on view unload/disposing.
- Or have AutolootManager accept handlers via a Register/Unregister API and store WeakReference targets.
Example using weak references (illustrative):
var weak = new WeakReference<AutolootViewModel>(this);
_manager.SetEnabled = val => { if (weak.TryGetTarget(out var vm)) vm.Enabled = val; };
_manager.IsEnabled = () => weak.TryGetTarget(out var vm) && vm.Enabled;
// repeat for others...🤖 Prompt for AI Agents
ClassicAssist/UI/ViewModels/Agents/AutolootViewModel.cs around lines 103-112
assigns instance-bound delegates to the singleton AutolootManager which captures
the ViewModel and prevents GC; fix by adding a DetachFromManager() method that
clears or replaces every manager delegate (GetEntries, CheckContainer,
CheckItems, IsRunning, MatchTextValue, IsEnabled, SetEnabled) with null/no-op
handlers and call DetachFromManager() from the view unload/dispose path, or
refactor to a Register/Unregister API on AutolootManager (store handlers there)
or wrap the ViewModel in WeakReference when assigning delegates and use
TryGetTarget checks so the delegates do not hold strong references to the
ViewModel.
|



Moves Autoloot property loading and assembly loading logic to AutolootManager. This change centralizes the property management, and removes redundant loading logic from the ECV and Autoloot View Models.
Summary by CodeRabbit
New Features
Changes
Refactor
Chores