├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md └── src ├── Directory.Build.props ├── HackF5.UnitySpy.Gui.Avalonia ├── App.xaml ├── App.xaml.cs ├── Assets │ ├── Fonts │ │ ├── SourceCodePro-Black.ttf │ │ ├── SourceCodePro-BlackItalic.ttf │ │ ├── SourceCodePro-Bold.ttf │ │ ├── SourceCodePro-BoldItalic.ttf │ │ ├── SourceCodePro-ExtraLight.ttf │ │ ├── SourceCodePro-ExtraLightItalic.ttf │ │ ├── SourceCodePro-Italic.ttf │ │ ├── SourceCodePro-Light.ttf │ │ ├── SourceCodePro-LightItalic.ttf │ │ ├── SourceCodePro-Medium.ttf │ │ ├── SourceCodePro-MediumItalic.ttf │ │ ├── SourceCodePro-Regular.ttf │ │ ├── SourceCodePro-SemiBold.ttf │ │ └── SourceCodePro-SemiBoldItalic.ttf │ ├── error.png │ ├── info.png │ ├── question.png │ └── warning.png ├── GuiAutofacModule.cs ├── HackF5.UnitySpy.Gui.Avalonia.csproj ├── Mvvm │ ├── AsyncCommand.cs │ ├── AvaloniaMainThreadInvoker.cs │ ├── CommandCollection.cs │ ├── ExtendedContentControl.cs │ └── ViewLocator.cs ├── Program.cs ├── Styles │ ├── SideBar.xaml │ └── Styles.xaml ├── View │ ├── AssemblyImageView.xaml │ ├── AssemblyImageView.xaml.cs │ ├── Helper │ │ └── BoolToFontWeightConverter.cs │ ├── ListContentView.xaml │ ├── ListContentView.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── ManagedObjectInstanceContentView.xaml │ ├── ManagedObjectInstanceContentView.xaml.cs │ ├── MessageBox.xaml │ ├── MessageBox.xaml.cs │ ├── RawMemoryView.xaml │ ├── RawMemoryView.xaml.cs │ ├── TypeDefinitionContentView.xaml │ ├── TypeDefinitionContentView.xaml.cs │ ├── TypeDefinitionView.xaml │ └── TypeDefinitionView.xaml.cs └── ViewModel │ ├── AssemblyImageViewModel.cs │ ├── MainWindowViewModel.cs │ ├── ProcessViewModel.cs │ ├── RawMemoryViewModel.cs │ ├── TypeDefinitionContentViewModel.cs │ └── TypeDefinitionViewModel.cs ├── HackF5.UnitySpy.Gui.Wpf ├── App.config ├── App.xaml ├── App.xaml.cs ├── GuiAutofacModule.cs ├── HackF5.UnitySpy.Gui.Wpf.csproj ├── HackF5.UnitySpy.Gui.Wpf.csproj.DotSettings ├── Mvvm │ ├── AsyncCommand.cs │ ├── BindableCollection.cs │ ├── CommandCollection.cs │ ├── DialogService.cs │ ├── ExtendedContentControl.cs │ ├── ViewLocator.cs │ └── WpfMainThreadInvoker.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── View │ ├── AssemblyImageView.xaml │ ├── AssemblyImageView.xaml.cs │ ├── ListContentView.xaml │ ├── ListContentView.xaml.cs │ ├── MainView.xaml │ ├── MainView.xaml.cs │ ├── ManagedObjectInstanceContentView.xaml │ ├── ManagedObjectInstanceContentView.xaml.cs │ ├── TypeDefinitionContentView.xaml │ ├── TypeDefinitionContentView.xaml.cs │ ├── TypeDefinitionView.xaml │ └── TypeDefinitionView.xaml.cs ├── ViewModel │ ├── AssemblyImageViewModel.cs │ ├── MainViewModel.cs │ ├── ProcessViewModel.cs │ ├── TypeDefinitionContentViewModel.cs │ └── TypeDefinitionViewModel.cs └── packages.config ├── HackF5.UnitySpy.Gui ├── HackF5.UnitySpy.Gui.csproj ├── Mvvm │ ├── AutofacExtensions.cs │ ├── IMainThreadInvoker.cs │ ├── MainThreadInvoker.cs │ ├── PropertyChangedBase.cs │ ├── RegisterAttribute.cs │ └── RegistrationType.cs └── ViewModel │ ├── AppendToTrailEventArgs.cs │ ├── InstanceFieldViewModel.cs │ ├── ListContentViewModel.cs │ ├── ListItemViewModel.cs │ ├── ManagedObjectInstanceContentViewModel.cs │ └── StaticFieldViewModel.cs ├── HackF5.UnitySpy.Native ├── Makefile ├── linux.c ├── macos.c ├── server.c └── server.h ├── HackF5.UnitySpy.Tests ├── HackF5.UnitySpy.HearthstoneLib.Tests.csproj └── TestHearthstoneLibApi.cs ├── HackF5.UnitySpy.sln ├── HackF5.UnitySpy.sln.DotSettings ├── HackF5.UnitySpy.snk ├── HackF5.UnitySpy ├── AssemblyImageFactory.cs ├── Detail │ ├── AssemblyImage.cs │ ├── FieldDefinition.cs │ ├── ManagedClassInstance.cs │ ├── ManagedObjectInstance.cs │ ├── ManagedStructInstance.cs │ ├── MemoryObject.cs │ ├── MonoClassKind.cs │ ├── TypeCode.cs │ ├── TypeDefinition.cs │ └── TypeInfo.cs ├── HackF5.UnitySpy.csproj ├── IAssemblyImage.cs ├── IFieldDefinition.cs ├── IManagedObjectInstance.cs ├── IMemoryObject.cs ├── ITypeDefinition.cs ├── ITypeInfo.cs ├── Offsets │ ├── BinaryFormat.cs │ ├── MachOFormatOffsets.cs │ ├── MonoLibraryOffsets.cs │ ├── PEFormatOffsets.cs │ └── UnityVersion.cs ├── ProcessFacade │ ├── ModuleInfo.cs │ ├── ProcessFacade.cs │ ├── ProcessFacadeClient.cs │ ├── ProcessFacadeLinux.cs │ ├── ProcessFacadeLinuxClient.cs │ ├── ProcessFacadeLinuxDirect.cs │ ├── ProcessFacadeLinuxDump.cs │ ├── ProcessFacadeLinuxPTrace.cs │ ├── ProcessFacadeMacOS.cs │ ├── ProcessFacadeMacOSClient.cs │ ├── ProcessFacadeMacOSDirect.cs │ ├── ProcessFacadeWindows.cs │ └── UnityProcessFacade.cs └── Util │ ├── ByteArrayPool.cs │ ├── ConversionUtils.cs │ └── MemoryReadingUtils.cs └── rules.ruleset /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/src/HackF5.UnitySpy.Gui.Avalonia/bin/Debug/netcoreapp3.1/HackF5.UnitySpy.Gui.Avalonia", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/src/HackF5.UnitySpy.Gui.Avalonia/bin/Debug/", 15 | "stopAtEntry": false, 16 | "console": "internalConsole" 17 | } 18 | 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "detail": "Build UnitySpy with Avalionia GUI", 7 | "command": "dotnet", 8 | "type": "shell", 9 | "args": [ 10 | "build" 11 | ], 12 | "options": { 13 | "cwd": "${workspaceFolder}/src/HackF5.UnitySpy.Gui.Avalonia" 14 | }, 15 | "problemMatcher": "$msCompile" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 brian t 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unityspy 2 | 3 | See this [online article](https://medium.com/@hackf5/hacking-into-unity-games-ca99f87954c) for a walkthrough of what this lib is doing and some explanations about how it works 4 | If you're interested in contributing and making it evolve, don't hesitate to join the [Discord server](https://discord.gg/myk6Zn8rnY) 5 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | HackF5 4 | HackF5.UnitySpy 5 | dev@HackF5 6 | en-US 7 | 1.0.0.0 8 | 1.0.0.0 9 | 10 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia 2 | { 3 | using Autofac; 4 | using global::Avalonia; 5 | using global::Avalonia.Controls; 6 | using global::Avalonia.Controls.ApplicationLifetimes; 7 | using global::Avalonia.Markup.Xaml; 8 | using HackF5.UnitySpy.Gui.Mvvm; 9 | using HackF5.UnitySpy.Gui.Avalonia.Mvvm; 10 | using HackF5.UnitySpy.Gui.Avalonia.ViewModel; 11 | using HackF5.UnitySpy.Gui.Avalonia.View; 12 | 13 | public class App : Application 14 | { 15 | private IContainer container; 16 | 17 | public override void Initialize() 18 | { 19 | AvaloniaXamlLoader.Load(this); 20 | } 21 | 22 | public override void OnFrameworkInitializationCompleted() 23 | { 24 | MainThreadInvoker.Current = new AvaloniaMainThreadInvoker(); 25 | 26 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 27 | { 28 | MainWindow mainWindow = new MainWindow(); 29 | mainWindow.DataContext = new MainWindowViewModel(mainWindow); 30 | desktop.MainWindow = mainWindow; 31 | desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; 32 | } 33 | 34 | var builder = new ContainerBuilder(); 35 | builder.RegisterAssemblyModules(this.GetType().Assembly); 36 | this.container = builder.Build(); 37 | 38 | // Not sure why this is here. Is it needed? 39 | var theme = new global::Avalonia.Themes.Default.DefaultTheme(); 40 | theme.TryGetResource("Button", out _); 41 | 42 | base.OnFrameworkInitializationCompleted(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Black.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-BlackItalic.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-BoldItalic.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-ExtraLight.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Italic.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Light.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-LightItalic.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Medium.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-MediumItalic.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-SemiBold.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/Fonts/SourceCodePro-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/error.png -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/info.png -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/question.png -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Assets/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.Gui.Avalonia/Assets/warning.png -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/GuiAutofacModule.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia 2 | { 3 | using System.Reflection; 4 | using Autofac; 5 | using HackF5.UnitySpy.Gui.Avalonia.Mvvm; 6 | using HackF5.UnitySpy.Gui.Mvvm; 7 | using Module = Autofac.Module; 8 | 9 | public class GuiAutofacModule : Module 10 | { 11 | protected override void Load(ContainerBuilder builder) 12 | { 13 | var guiBaseAssembly = Assembly.Load("HackF5.UnitySpy.Gui"); 14 | builder.AutoRegisterAssemblyTypes(this.ThisAssembly); 15 | builder.AutoRegisterAssemblyTypes(guiBaseAssembly); 16 | builder.RegisterAssemblyTypes(this.ThisAssembly).Where(t => t.Name.EndsWith("ViewModel")); 17 | builder.RegisterAssemblyTypes(guiBaseAssembly).Where(t => t.Name.EndsWith("ViewModel")); 18 | ViewLocator.RegisterAssembly(this.ThisAssembly); 19 | ViewLocator.RegisterAssembly(guiBaseAssembly); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/HackF5.UnitySpy.Gui.Avalonia.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | netcoreapp3.1 5 | HackF5.UnitySpy.Gui.Avalonia 6 | x64 7 | true 8 | false 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | %(Filename) 16 | 17 | 18 | Designer 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Mvvm/AsyncCommand.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable SA1402 // File may only contain a single type 2 | 3 | namespace HackF5.UnitySpy.Gui.Avalonia.Mvvm 4 | { 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Windows.Input; 9 | using JetBrains.Annotations; 10 | 11 | [PublicAPI] 12 | public class AsyncCommand : AsyncCommand 13 | { 14 | public AsyncCommand([NotNull] Func execute, Func canExecute = default) 15 | : base(o => execute(), o => canExecute?.Invoke() ?? true) 16 | { 17 | } 18 | } 19 | 20 | [PublicAPI] 21 | public class AsyncCommand : ICommand 22 | { 23 | private readonly Func canExecute; 24 | 25 | private readonly Func execute; 26 | 27 | private volatile int isExecutingFlag; 28 | 29 | public AsyncCommand( 30 | [NotNull] Func execute, 31 | Func canExecute = default) 32 | { 33 | this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); 34 | this.canExecute = canExecute ?? (o => true); 35 | } 36 | 37 | public event EventHandler CanExecuteChanged; 38 | // { 39 | // add => CommandManager.RequerySuggested += value; 40 | // remove => CommandManager.RequerySuggested -= value; 41 | // } 42 | 43 | public bool IsExecuting => this.isExecutingFlag == 1; 44 | 45 | public Task Task { get; private set; } = Task.CompletedTask; 46 | 47 | public bool CanExecute(object parameter) => 48 | !this.IsExecuting && this.canExecute((T)parameter); 49 | 50 | public async void Execute(object parameter) 51 | { 52 | if (Interlocked.CompareExchange(ref this.isExecutingFlag, 1, 0) != 0) 53 | { 54 | // prevent reentrancy. 55 | return; 56 | } 57 | 58 | var tcs = new TaskCompletionSource(); 59 | this.Task = tcs.Task; 60 | 61 | try 62 | { 63 | this.ChangeCanExecute(); 64 | 65 | await this.execute((T)parameter); 66 | tcs.SetResult(null); 67 | } 68 | catch (Exception ex) 69 | { 70 | tcs.SetException(ex); 71 | throw; 72 | } 73 | finally 74 | { 75 | Interlocked.Exchange(ref this.isExecutingFlag, 0); 76 | this.ChangeCanExecute(); 77 | } 78 | } 79 | 80 | public void ChangeCanExecute() 81 | { 82 | //CommandManager.InvalidateRequerySuggested(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Mvvm/AvaloniaMainThreadInvoker.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.Mvvm 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using global::Avalonia; 6 | using global::Avalonia.Threading; 7 | using HackF5.UnitySpy.Gui.Mvvm; 8 | using JetBrains.Annotations; 9 | 10 | public class AvaloniaMainThreadInvoker : MainThreadInvoker 11 | { 12 | protected override bool CheckAccess() => Application.Current.CheckAccess(); 13 | 14 | protected override void Invoke(Action action) => Dispatcher.UIThread.Post(action); 15 | 16 | protected override Task InvokeAsync(Action action) => Dispatcher.UIThread.InvokeAsync(action); 17 | } 18 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Mvvm/CommandCollection.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.Mvvm 2 | { 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading.Tasks; 7 | using System.Windows.Input; 8 | using HackF5.UnitySpy.Gui.Mvvm; 9 | using JetBrains.Annotations; 10 | 11 | [UsedImplicitly] 12 | [Register(RegistrationType.Transient)] 13 | public class CommandCollection 14 | { 15 | private readonly ConcurrentDictionary commands = new ConcurrentDictionary(); 16 | 17 | public AsyncCommand CreateAsyncCommand( 18 | [NotNull] Func execute, 19 | Func canExecute = default, 20 | [CallerMemberName] string propertyName = default) 21 | { 22 | return (AsyncCommand)this.commands.GetOrAdd( 23 | propertyName ?? throw new ArgumentException("Property name cannot be null.", nameof(propertyName)), 24 | _ => new AsyncCommand(execute, canExecute)); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Mvvm/ExtendedContentControl.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.Mvvm 2 | { 3 | using System; 4 | using global::Avalonia; 5 | using global::Avalonia.Controls; 6 | using global::Avalonia.Styling; 7 | 8 | public class ExtendedContentControl : ContentControl, IStyleable 9 | { 10 | Type IStyleable.StyleKey => typeof(ContentControl); 11 | 12 | public static readonly DirectProperty ExtendedContentProperty = 13 | AvaloniaProperty.RegisterDirect( 14 | nameof(ExtendedContent), 15 | o => o.ExtendedContent, 16 | OnExtendedContentChanged); 17 | 18 | public object ExtendedContent 19 | { 20 | get => (object)this.GetValue(ExtendedContentControl.ExtendedContentProperty); 21 | set => this.SetValue(ExtendedContentControl.ExtendedContentProperty, value); 22 | } 23 | 24 | private static void OnExtendedContentChanged(ExtendedContentControl control, object value) 25 | { 26 | control.Content = ViewLocator.GetViewFor(value); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Mvvm/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.Mvvm 2 | { 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using global::Avalonia.Controls; 9 | using Autofac.Util; 10 | using JetBrains.Annotations; 11 | 12 | public static class ViewLocator 13 | { 14 | private static readonly HashSet RegisteredAssemblies = new HashSet(); 15 | 16 | private static readonly ConcurrentDictionary> ViewFactories = 17 | new ConcurrentDictionary>(); 18 | 19 | private static readonly Dictionary ViewTypes = new Dictionary(); 20 | 21 | public static Control GetViewFor(object viewModel) 22 | { 23 | if (viewModel == default) 24 | { 25 | return default; 26 | } 27 | 28 | var view = ViewLocator.ViewFactories.GetOrAdd(viewModel.GetType(), ViewLocator.GetViewFactory)(); 29 | view.DataContext = viewModel; 30 | return view; 31 | } 32 | 33 | public static void RegisterAssembly(Assembly assembly) 34 | { 35 | if (!ViewLocator.RegisteredAssemblies.Add(assembly)) 36 | { 37 | return; 38 | } 39 | 40 | var assemblyTypes = assembly.GetLoadableTypes() 41 | .Where( 42 | t => 43 | t.IsSubclassOf(typeof(UserControl)) 44 | && t.Name.EndsWith("View") 45 | /*&& (t.GetConstructor(Array.Empty()) != default)*/); 46 | 47 | foreach (var assemblyType in assemblyTypes) 48 | { 49 | // ReSharper disable once AssignNullToNotNullAttribute 50 | ViewLocator.ViewTypes[assemblyType.Name] = assemblyType; 51 | } 52 | } 53 | 54 | private static Func GetViewFactory([NotNull] Type viewModelType) 55 | { 56 | // ReSharper disable once PossibleNullReferenceException 57 | var key = viewModelType.Name.Replace("ViewModel", "View"); 58 | 59 | if (!ViewLocator.ViewTypes.TryGetValue(key, out var viewType)) 60 | { 61 | return () => new TextBlock { Text = $"No view exists for view model of type {viewModelType}." }; 62 | } 63 | 64 | return () => (Control)Activator.CreateInstance(viewType); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Program.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia 2 | { 3 | using System; 4 | using global::Avalonia; 5 | using global::Avalonia.Controls.ApplicationLifetimes; 6 | using global::Avalonia.ReactiveUI; 7 | 8 | class Program 9 | { 10 | // Initialization code. Don't use any Avalonia, third-party APIs or any 11 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 12 | // yet and stuff might break. 13 | public static void Main(string[] args) => BuildAvaloniaApp() 14 | .StartWithClassicDesktopLifetime(args); 15 | 16 | // Avalonia configuration, don't remove; also used by visual designer. 17 | public static AppBuilder BuildAvaloniaApp() 18 | => AppBuilder.Configure() 19 | .UsePlatformDetect() 20 | .With(new SkiaOptions{ MaxGpuResourceSizeBytes = 8096000}) 21 | .With(new Win32PlatformOptions{ AllowEglInitialization = true }) 22 | .LogToTrace() 23 | .UseReactiveUI(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Styles/SideBar.xaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 53 | 54 | 64 | 67 | 70 | 71 | 74 | 75 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/Styles/Styles.xaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 40 0 40 0 12 | avares://XamlControlsGallery/Assets/Fonts#Source Code Pro 13 | 14 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/AssemblyImageView.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 39 | Types with static fields only 40 | 41 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 64 | 65 | 68 | 69 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/AssemblyImageView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View 2 | { 3 | using global::Avalonia.Controls; 4 | using global::Avalonia.Markup.Xaml; 5 | 6 | public class AssemblyImageView : UserControl 7 | { 8 | private readonly ListBox definitionsList; 9 | 10 | public AssemblyImageView() 11 | { 12 | this.InitializeComponent(); 13 | this.definitionsList = this.FindControl("DefinitionsList"); 14 | } 15 | 16 | protected void InitializeComponent() 17 | { 18 | AvaloniaXamlLoader.Load(this); 19 | //this.AttachDevTools(); 20 | } 21 | 22 | private void DefinitionsList_OnSelectionChanged(object sender, SelectionChangedEventArgs e) 23 | { 24 | this.definitionsList.ScrollIntoView(this.definitionsList.SelectedItem); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/Helper/BoolToFontWeightConverter.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View.Helpers 2 | { 3 | using System; 4 | using System.Globalization; 5 | using global::Avalonia.Media; 6 | using global::Avalonia.Data.Converters; 7 | 8 | /// 9 | /// Converts a null value to parameter 10 | /// 11 | public class BoolToFontWeightConverter : IValueConverter 12 | { 13 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 14 | { 15 | return (bool) value ? (FontWeight)Enum.Parse(typeof(FontWeight), parameter.ToString()) : FontWeight.Normal; 16 | } 17 | 18 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 19 | { 20 | throw new NotSupportedException(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/ListContentView.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/ListContentView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View 2 | { 3 | using global::Avalonia.Controls; 4 | using global::Avalonia.Interactivity; 5 | using global::Avalonia.Markup.Xaml; 6 | using HackF5.UnitySpy.Gui.ViewModel; 7 | 8 | public class ListContentView : UserControl 9 | { 10 | private readonly ListBox itemsList; 11 | 12 | public ListContentView() 13 | { 14 | this.InitializeComponent(); 15 | this.itemsList = this.FindControl("ItemsList"); 16 | } 17 | 18 | protected void InitializeComponent() 19 | { 20 | AvaloniaXamlLoader.Load(this); 21 | //this.AttachDevTools(); 22 | } 23 | 24 | public void Control_OnMouseDoubleClick(object sender, RoutedEventArgs e) 25 | { 26 | if (!(this.itemsList.SelectedItem is ListItemViewModel item)) 27 | { 28 | return; 29 | } 30 | 31 | if (!(this.DataContext is ListContentViewModel model)) 32 | { 33 | return; 34 | } 35 | 36 | model.OnAppendToTrail(item.Index.ToString()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Mode 37 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Process 53 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Maps File 73 | 74 | 75 | 76 | Dump Files Path 77 | 78 | 79 | 80 | Mem File 81 | 82 | 83 | 84 | Game Executable File 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View 2 | { 3 | using global::Avalonia.Controls; 4 | using global::Avalonia.Markup.Xaml; 5 | 6 | public class MainWindow : Window 7 | { 8 | public MainWindow() 9 | { 10 | this.InitializeComponent(); 11 | } 12 | 13 | protected void InitializeComponent() 14 | { 15 | AvaloniaXamlLoader.Load(this); 16 | //this.AttachDevTools(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/ManagedObjectInstanceContentView.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 20 | 21 | 22 | 23 | 26 | 27 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/ManagedObjectInstanceContentView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View 2 | { 3 | using global::Avalonia.Controls; 4 | using global::Avalonia.Interactivity; 5 | using global::Avalonia.Markup.Xaml; 6 | using HackF5.UnitySpy.Gui.ViewModel; 7 | 8 | public class ManagedObjectInstanceContentView : UserControl 9 | { 10 | private readonly ListBox itemsList; 11 | 12 | public ManagedObjectInstanceContentView() 13 | { 14 | this.InitializeComponent(); 15 | this.itemsList = this.FindControl("ItemsList"); 16 | } 17 | 18 | protected void InitializeComponent() 19 | { 20 | AvaloniaXamlLoader.Load(this); 21 | //this.AttachDevTools(); 22 | } 23 | 24 | public void Control_OnMouseDoubleClick(object sender, RoutedEventArgs e) 25 | { 26 | if (!(this.itemsList.SelectedItem is InstanceFieldViewModel item)) 27 | { 28 | return; 29 | } 30 | 31 | if (!(this.DataContext is ManagedObjectInstanceContentViewModel model)) 32 | { 33 | return; 34 | } 35 | 36 | model.OnAppendToTrail(item.Name); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/MessageBox.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/RawMemoryView.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Start Address 19 | 20 | Buffer Size 21 | 24 | 256 25 | 512 26 | 1024 27 | 28 | 29 | 30 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/RawMemoryView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using global::Avalonia.Controls; 6 | using global::Avalonia.Markup.Xaml; 7 | using HackF5.UnitySpy.Gui.Avalonia.ViewModel; 8 | 9 | public class RawMemoryView : Window 10 | { 11 | private readonly RawMemoryViewModel viewModel; 12 | 13 | public RawMemoryView() 14 | { 15 | viewModel = new RawMemoryViewModel(); 16 | DataContext = viewModel; 17 | 18 | this.InitializeComponent(); 19 | 20 | // Hide the window instead of closing it (cannot re-show a closed window) 21 | Closing += (s, e) => 22 | { 23 | ((Window)s).Hide(); 24 | e.Cancel = true; 25 | }; 26 | } 27 | 28 | protected void InitializeComponent() 29 | { 30 | AvaloniaXamlLoader.Load(this); 31 | //this.AttachDevTools(); 32 | } 33 | 34 | 35 | public Task ShowDialog(Window owner, IntPtr address) 36 | { 37 | this.viewModel.StartAddress = address.ToString("X"); 38 | return ShowDialog(owner); 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/TypeDefinitionContentView.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 22 | 23 | 24 | 25 | 28 | 29 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/TypeDefinitionContentView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View 2 | { 3 | using global::Avalonia.Controls; 4 | using global::Avalonia.Input; 5 | using global::Avalonia.Interactivity; 6 | using global::Avalonia.Markup.Xaml; 7 | using HackF5.UnitySpy.Gui.ViewModel; 8 | using HackF5.UnitySpy.Gui.Avalonia.ViewModel; 9 | 10 | public class TypeDefinitionContentView : UserControl 11 | { 12 | private readonly ListBox itemsList; 13 | 14 | public TypeDefinitionContentView() 15 | { 16 | this.InitializeComponent(); 17 | this.itemsList = this.FindControl("ItemsList"); 18 | } 19 | 20 | protected void InitializeComponent() 21 | { 22 | AvaloniaXamlLoader.Load(this); 23 | //this.AttachDevTools(); 24 | } 25 | 26 | public void Control_OnMouseDoubleClick(object sender, RoutedEventArgs e) 27 | { 28 | if (!(this.itemsList.SelectedItem is StaticFieldViewModel item)) 29 | { 30 | return; 31 | } 32 | 33 | if (!(this.DataContext is TypeDefinitionContentViewModel model)) 34 | { 35 | return; 36 | } 37 | 38 | model.OnAppendToTrail(item.Name); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/TypeDefinitionView.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 | 38 | 44 | 45 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/View/TypeDefinitionView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.View 2 | { 3 | using global::Avalonia.Controls; 4 | using global::Avalonia.Markup.Xaml; 5 | 6 | public class TypeDefinitionView : UserControl 7 | { 8 | public TypeDefinitionView() 9 | { 10 | this.InitializeComponent(); 11 | this.Initialized += (sender, args) => this.FindControl("PathTextBox").Focus(); 12 | } 13 | 14 | protected void InitializeComponent() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | //this.AttachDevTools(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/ViewModel/AssemblyImageViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.ViewModel 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.Linq; 7 | using global::Avalonia.Threading; 8 | using JetBrains.Annotations; 9 | using ReactiveUI; 10 | 11 | public class AssemblyImageViewModel : ReactiveObject 12 | { 13 | private readonly IAssemblyImage image; 14 | 15 | private readonly List types; 16 | 17 | private readonly TypeDefinitionViewModel.Factory typeDefinitionFactory; 18 | 19 | private TypeDefinitionViewModel selectedType; 20 | 21 | private bool hasStaticOnly = true; 22 | 23 | private string listViewFilter = ""; 24 | 25 | public AssemblyImageViewModel( 26 | [NotNull] IAssemblyImage image, 27 | [NotNull] TypeDefinitionViewModel.Factory typeDefinitionFactory) 28 | { 29 | this.image = image ?? throw new ArgumentNullException(nameof(image)); 30 | 31 | this.typeDefinitionFactory = 32 | typeDefinitionFactory ?? throw new ArgumentNullException(nameof(typeDefinitionFactory)); 33 | 34 | this.types = new List(); 35 | Dispatcher.UIThread.InvokeAsync(this.Init); 36 | } 37 | 38 | public delegate AssemblyImageViewModel Factory(IAssemblyImage image); 39 | 40 | public TypeDefinitionViewModel SelectedType 41 | { 42 | get => this.selectedType; 43 | set => this.RaiseAndSetIfChanged(ref this.selectedType, value); 44 | } 45 | 46 | public bool HasStaticOnly 47 | { 48 | get => this.hasStaticOnly; 49 | set 50 | { 51 | if(this.hasStaticOnly != value) 52 | { 53 | this.RaiseAndSetIfChanged(ref this.hasStaticOnly, value); 54 | this.Refresh(); 55 | } 56 | } 57 | } 58 | 59 | public string ListViewFilter 60 | { 61 | get => this.listViewFilter; 62 | set 63 | { 64 | bool refresh = value != this.listViewFilter; 65 | this.RaiseAndSetIfChanged(ref this.listViewFilter, value); 66 | if(refresh) 67 | { 68 | this.Refresh(); 69 | } 70 | } 71 | } 72 | 73 | public ObservableCollection FilteredTypes { get; } = 74 | new ObservableCollection(); 75 | 76 | private void Refresh() 77 | { 78 | this.FilteredTypes.Clear(); 79 | 80 | types.ForEach(definition => 81 | { 82 | if(Filter(definition)) 83 | { 84 | this.FilteredTypes.Add(definition); 85 | } 86 | }); 87 | } 88 | 89 | private bool Filter(TypeDefinitionViewModel definition) 90 | { 91 | if (this.hasStaticOnly && !definition.HasStaticFields) 92 | { 93 | return false; 94 | } 95 | 96 | var text = this.listViewFilter.Trim(); 97 | if (string.IsNullOrEmpty(text)) 98 | { 99 | return true; 100 | } 101 | 102 | if (!(definition.FullName.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0)) 103 | { 104 | return false; 105 | } 106 | 107 | return true; 108 | } 109 | 110 | private void Init() 111 | { 112 | var typeDefinitionsOrdered = this.image.TypeDefinitions.OrderBy(td => td.FullName); 113 | 114 | this.types.AddRange(new List(typeDefinitionsOrdered.Select(td => this.typeDefinitionFactory(td)))); 115 | this.Refresh(); 116 | 117 | this.SelectedType = this.FilteredTypes.FirstOrDefault(t => t.FullName == "CollectionManager"); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/ViewModel/ProcessViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.ViewModel 2 | { 3 | using System.Diagnostics; 4 | using ReactiveUI; 5 | 6 | public class ProcessViewModel : ReactiveObject 7 | { 8 | private readonly Process process; 9 | 10 | public int Id => process.Id; 11 | 12 | public string Name => process.ProcessName; 13 | 14 | public string NameAndId => Name + "(" + Id + ")"; 15 | 16 | public ProcessViewModel(Process process) 17 | { 18 | this.process = process; 19 | } 20 | 21 | public override int GetHashCode() 22 | { 23 | return process.GetHashCode() * 7; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/ViewModel/RawMemoryViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.ViewModel 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.Reactive; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using global::Avalonia.Controls; 10 | using HackF5.UnitySpy.ProcessFacade; 11 | using HackF5.UnitySpy.Util; 12 | using ReactiveUI; 13 | 14 | public class RawMemoryViewModel : ReactiveObject 15 | { 16 | private ProcessFacade process; 17 | 18 | private string bufferSize = "1024"; 19 | 20 | private string startAddress; 21 | 22 | private byte[] buffer; 23 | 24 | public RawMemoryViewModel() 25 | { 26 | this.Refresh = ReactiveCommand.Create(this.StartRefresh); 27 | this.StartRefresh(); 28 | } 29 | 30 | public ProcessFacade Process 31 | { 32 | set { 33 | if (this.process != value) 34 | { 35 | this.process = value; 36 | this.StartRefresh(); 37 | } 38 | } 39 | } 40 | 41 | public string BufferSize 42 | { 43 | get => this.bufferSize; 44 | set { 45 | if(this.bufferSize != value) 46 | { 47 | this.RaiseAndSetIfChanged(ref this.bufferSize, value); 48 | this.InitBuffer(); 49 | this.StartRefresh(); 50 | } 51 | } 52 | } 53 | 54 | public string StartAddress 55 | { 56 | get => this.startAddress; 57 | set { 58 | if(this.startAddress != value) 59 | { 60 | this.RaiseAndSetIfChanged(ref this.startAddress, value); 61 | this.StartRefresh(); 62 | } 63 | } 64 | } 65 | 66 | public ObservableCollection BufferLines { get; } = 67 | new ObservableCollection(); 68 | 69 | public ReactiveCommand Refresh { get; } 70 | 71 | private void InitBuffer() 72 | { 73 | this.buffer = new byte[int.Parse(this.bufferSize)]; 74 | } 75 | 76 | private void StartRefresh() 77 | { 78 | Task.Run(() => this.ExecuteRefresh()); 79 | } 80 | 81 | private void ExecuteRefresh() 82 | { 83 | if (this.process == null) 84 | { 85 | return; 86 | } 87 | 88 | if (this.buffer == null) 89 | { 90 | this.InitBuffer(); 91 | } 92 | IntPtr startAddressPtr = new IntPtr(Convert.ToInt64(this.startAddress, 16)); 93 | this.process.ReadProcessMemory(this.buffer, startAddressPtr, true, this.buffer.Length); 94 | 95 | this.BufferLines.Clear(); 96 | 97 | int bytesPerLine = 32; 98 | for (int i = 0; i < this.buffer.Length; i += bytesPerLine) 99 | { 100 | IntPtr address = startAddressPtr + i; 101 | StringBuilder line = new StringBuilder(address.ToString("X")); 102 | line.Append(": "); 103 | StringBuilder charLine = new StringBuilder(); 104 | for (int j = 0; j < bytesPerLine; j++) 105 | { 106 | line.Append(buffer[i + j].ToString("X2")); 107 | line.Append(' '); 108 | charLine.Append(ToAsciiSymbol(this.buffer[i + j])); 109 | } 110 | line.Append(charLine); 111 | this.BufferLines.Add(line.ToString()); 112 | } 113 | } 114 | 115 | private static char ToAsciiSymbol(byte value) 116 | { 117 | if (value < 32) return '.'; // Non-printable ASCII 118 | if (value < 127) return (char)value; // Normal ASCII 119 | // Handle the hole in Latin-1 120 | if (value == 127) return '.'; 121 | if (value < 0x90) return "€.‚ƒ„…†‡ˆ‰Š‹Œ.Ž."[ value & 0xF ]; 122 | if (value < 0xA0) return ".‘’“”•–—˜™š›œ.žŸ"[ value & 0xF ]; 123 | if (value == 0xAD) return '.'; // Soft hyphen: this symbol is zero-width even in monospace fonts 124 | return (char) value; // Normal Latin-1 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Avalonia/ViewModel/TypeDefinitionContentViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Avalonia.ViewModel 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using JetBrains.Annotations; 7 | using HackF5.UnitySpy.Gui.ViewModel; 8 | using ReactiveUI; 9 | 10 | public class TypeDefinitionContentViewModel : ReactiveObject 11 | { 12 | private readonly ITypeDefinition definition; 13 | 14 | public TypeDefinitionContentViewModel( 15 | [NotNull] ITypeDefinition definition, 16 | StaticFieldViewModel.Factory fieldFactory) 17 | { 18 | this.definition = definition ?? throw new ArgumentNullException(nameof(definition)); 19 | this.StaticFields = this.definition.Fields.Where(f => f.TypeInfo.IsStatic && !f.TypeInfo.IsConstant) 20 | .Select(f => fieldFactory(f)) 21 | .ToArray(); 22 | } 23 | 24 | public delegate TypeDefinitionContentViewModel Factory(ITypeDefinition definition); 25 | 26 | public event EventHandler AppendToTrail; 27 | 28 | public IEnumerable StaticFields { get; } 29 | 30 | public virtual void OnAppendToTrail(string value) 31 | { 32 | this.AppendToTrail?.Invoke(this, new AppendToTrailEventArgs(value)); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf 2 | { 3 | using System.Windows; 4 | using Autofac; 5 | using HackF5.UnitySpy.Gui.Mvvm; 6 | using HackF5.UnitySpy.Gui.Wpf.Mvvm; 7 | using HackF5.UnitySpy.Gui.Wpf.ViewModel; 8 | 9 | public partial class App 10 | { 11 | private IContainer container; 12 | 13 | protected override void OnStartup(StartupEventArgs e) 14 | { 15 | MainThreadInvoker.Current = new WpfMainThreadInvoker(); 16 | 17 | base.OnStartup(e); 18 | var builder = new ContainerBuilder(); 19 | builder.RegisterAssemblyModules(this.GetType().Assembly); 20 | this.container = builder.Build(); 21 | 22 | this.MainWindow = (Window)ViewLocator.GetViewFor(this.container.Resolve()); 23 | this.MainWindow.Show(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/GuiAutofacModule.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf 2 | { 3 | using System.Reflection; 4 | using Autofac; 5 | using HackF5.UnitySpy.Gui.Mvvm; 6 | using HackF5.UnitySpy.Gui.Wpf.Mvvm; 7 | using Module = Autofac.Module; 8 | 9 | public class GuiAutofacModule : Module 10 | { 11 | protected override void Load(ContainerBuilder builder) 12 | { 13 | var guiBaseAssembly = Assembly.Load("HackF5.UnitySpy.Gui"); 14 | builder.AutoRegisterAssemblyTypes(this.ThisAssembly); 15 | builder.AutoRegisterAssemblyTypes(guiBaseAssembly); 16 | builder.RegisterAssemblyTypes(this.ThisAssembly).Where(t => t.Name.EndsWith("ViewModel")); 17 | builder.RegisterAssemblyTypes(guiBaseAssembly).Where(t => t.Name.EndsWith("ViewModel")); 18 | ViewLocator.RegisterAssembly(this.ThisAssembly); 19 | ViewLocator.RegisterAssembly(guiBaseAssembly); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/HackF5.UnitySpy.Gui.Wpf.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | No -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Mvvm/AsyncCommand.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable SA1402 // File may only contain a single type 2 | 3 | namespace HackF5.UnitySpy.Gui.Wpf.Mvvm 4 | { 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Windows.Input; 9 | using JetBrains.Annotations; 10 | 11 | [PublicAPI] 12 | public class AsyncCommand : AsyncCommand 13 | { 14 | public AsyncCommand([NotNull] Func execute, Func canExecute = default) 15 | : base(o => execute(), o => canExecute?.Invoke() ?? true) 16 | { 17 | } 18 | } 19 | 20 | [PublicAPI] 21 | public class AsyncCommand : ICommand 22 | { 23 | private readonly Func canExecute; 24 | 25 | private readonly Func execute; 26 | 27 | private volatile int isExecutingFlag; 28 | 29 | public AsyncCommand( 30 | [NotNull] Func execute, 31 | Func canExecute = default) 32 | { 33 | this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); 34 | this.canExecute = canExecute ?? (o => true); 35 | } 36 | 37 | public event EventHandler CanExecuteChanged 38 | { 39 | add => CommandManager.RequerySuggested += value; 40 | remove => CommandManager.RequerySuggested -= value; 41 | } 42 | 43 | public bool IsExecuting => this.isExecutingFlag == 1; 44 | 45 | public Task Task { get; private set; } = Task.CompletedTask; 46 | 47 | public bool CanExecute(object parameter) => 48 | !this.IsExecuting && this.canExecute((T)parameter); 49 | 50 | public async void Execute(object parameter) 51 | { 52 | if (Interlocked.CompareExchange(ref this.isExecutingFlag, 1, 0) != 0) 53 | { 54 | // prevent reentrancy. 55 | return; 56 | } 57 | 58 | var tcs = new TaskCompletionSource(); 59 | this.Task = tcs.Task; 60 | 61 | try 62 | { 63 | this.ChangeCanExecute(); 64 | 65 | await this.execute((T)parameter); 66 | tcs.SetResult(null); 67 | } 68 | catch (Exception ex) 69 | { 70 | tcs.SetException(ex); 71 | throw; 72 | } 73 | finally 74 | { 75 | Interlocked.Exchange(ref this.isExecutingFlag, 0); 76 | this.ChangeCanExecute(); 77 | } 78 | } 79 | 80 | public void ChangeCanExecute() 81 | { 82 | CommandManager.InvalidateRequerySuggested(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Mvvm/BindableCollection.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.Mvvm 2 | { 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Collections.Specialized; 6 | using System.ComponentModel; 7 | using System.Linq; 8 | using HackF5.UnitySpy.Gui.Mvvm; 9 | using JetBrains.Annotations; 10 | 11 | [PublicAPI] 12 | public class BindableCollection : ObservableCollection 13 | { 14 | public BindableCollection() 15 | { 16 | } 17 | 18 | public BindableCollection([NotNull] IEnumerable collection) 19 | : base(collection) 20 | { 21 | } 22 | 23 | public bool IsNotifying { get; set; } = true; 24 | 25 | public void Refresh() 26 | { 27 | void Action() 28 | { 29 | this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); 30 | this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); 31 | this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 32 | } 33 | 34 | MainThreadInvoker.Current.InvokeOnMainThread(Action); 35 | } 36 | 37 | public IReadOnlyList ToThreadSafeList() 38 | { 39 | var result = new List(); 40 | MainThreadInvoker.Current.InvokeOnMainThread(() => result.AddRange(this.ToArray())); 41 | return result; 42 | } 43 | 44 | protected override void ClearItems() => MainThreadInvoker.Current.InvokeOnMainThread(base.ClearItems); 45 | 46 | protected override void InsertItem(int index, T item) => 47 | MainThreadInvoker.Current.InvokeOnMainThread(() => base.InsertItem(index, item)); 48 | 49 | protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 50 | { 51 | if (!this.IsNotifying) 52 | { 53 | return; 54 | } 55 | 56 | MainThreadInvoker.Current.InvokeOnMainThread(() => base.OnCollectionChanged(e)); 57 | } 58 | 59 | protected override void OnPropertyChanged(PropertyChangedEventArgs e) 60 | { 61 | if (!this.IsNotifying) 62 | { 63 | return; 64 | } 65 | 66 | MainThreadInvoker.Current.InvokeOnMainThread(() => base.OnPropertyChanged(e)); 67 | } 68 | 69 | protected override void RemoveItem(int index) => 70 | MainThreadInvoker.Current.InvokeOnMainThread(() => base.RemoveItem(index)); 71 | 72 | protected override void SetItem(int index, T item) => 73 | MainThreadInvoker.Current.InvokeOnMainThread(() => base.SetItem(index, item)); 74 | } 75 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Mvvm/CommandCollection.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.Mvvm 2 | { 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading.Tasks; 7 | using System.Windows.Input; 8 | using HackF5.UnitySpy.Gui.Mvvm; 9 | using JetBrains.Annotations; 10 | 11 | [UsedImplicitly] 12 | [Register(RegistrationType.Transient)] 13 | public class CommandCollection 14 | { 15 | private readonly ConcurrentDictionary commands = new ConcurrentDictionary(); 16 | 17 | public AsyncCommand CreateAsyncCommand( 18 | [NotNull] Func execute, 19 | Func canExecute = default, 20 | [CallerMemberName] string propertyName = default) 21 | { 22 | return (AsyncCommand)this.commands.GetOrAdd( 23 | propertyName ?? throw new ArgumentException("Property name cannot be null.", nameof(propertyName)), 24 | _ => new AsyncCommand(execute, canExecute)); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Mvvm/DialogService.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.Mvvm 2 | { 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using HackF5.UnitySpy.Gui.Mvvm; 6 | using JetBrains.Annotations; 7 | 8 | [PublicAPI] 9 | public static class DialogService 10 | { 11 | public static Task ShowAsync(string title, string description) 12 | { 13 | return MainThreadInvoker.Current.InvokeOnMainThreadAsync(() => MessageBox.Show(description, title)); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Mvvm/ExtendedContentControl.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.Mvvm 2 | { 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | 6 | public class ExtendedContentControl : ContentControl 7 | { 8 | public static readonly DependencyProperty ExtendedContentProperty = DependencyProperty.Register( 9 | "ExtendedContent", 10 | typeof(object), 11 | typeof(ExtendedContentControl), 12 | new PropertyMetadata(default, ExtendedContentControl.OnExtendedContentChanged)); 13 | 14 | public object ExtendedContent 15 | { 16 | get => (object)this.GetValue(ExtendedContentControl.ExtendedContentProperty); 17 | set => this.SetValue(ExtendedContentControl.ExtendedContentProperty, value); 18 | } 19 | 20 | private static void OnExtendedContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 21 | { 22 | var control = (ExtendedContentControl)d; 23 | control.Content = ViewLocator.GetViewFor(e.NewValue); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Mvvm/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.Mvvm 2 | { 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Windows; 9 | using System.Windows.Controls; 10 | using Autofac.Util; 11 | using JetBrains.Annotations; 12 | 13 | public static class ViewLocator 14 | { 15 | private static readonly HashSet RegisteredAssemblies = new HashSet(); 16 | 17 | private static readonly ConcurrentDictionary> ViewFactories = 18 | new ConcurrentDictionary>(); 19 | 20 | private static readonly Dictionary ViewTypes = new Dictionary(); 21 | 22 | public static FrameworkElement GetViewFor(object viewModel) 23 | { 24 | if (viewModel == default) 25 | { 26 | return default; 27 | } 28 | 29 | var view = ViewLocator.ViewFactories.GetOrAdd(viewModel.GetType(), ViewLocator.GetViewFactory)(); 30 | view.DataContext = viewModel; 31 | return view; 32 | } 33 | 34 | public static void RegisterAssembly(Assembly assembly) 35 | { 36 | if (!ViewLocator.RegisteredAssemblies.Add(assembly)) 37 | { 38 | return; 39 | } 40 | 41 | var assemblyTypes = assembly.GetLoadableTypes() 42 | .Where( 43 | t => 44 | t.IsSubclassOf(typeof(FrameworkElement)) 45 | && t.Name.EndsWith("View") 46 | && (t.GetConstructor(Array.Empty()) != default)); 47 | 48 | foreach (var assemblyType in assemblyTypes) 49 | { 50 | // ReSharper disable once AssignNullToNotNullAttribute 51 | ViewLocator.ViewTypes[assemblyType.Name] = assemblyType; 52 | } 53 | } 54 | 55 | private static Func GetViewFactory([NotNull] Type viewModelType) 56 | { 57 | // ReSharper disable once PossibleNullReferenceException 58 | var key = viewModelType.Name.Replace("ViewModel", "View"); 59 | 60 | if (!ViewLocator.ViewTypes.TryGetValue(key, out var viewType)) 61 | { 62 | return () => new TextBlock { Text = $"No view exists for view model of type {viewModelType}." }; 63 | } 64 | 65 | return () => (FrameworkElement)Activator.CreateInstance(viewType); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Mvvm/WpfMainThreadInvoker.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.Mvvm 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using HackF5.UnitySpy.Gui.Mvvm; 7 | 8 | public class WpfMainThreadInvoker : MainThreadInvoker 9 | { 10 | protected override bool CheckAccess() => Application.Current.CheckAccess(); 11 | 12 | protected override void Invoke(Action action) => Application.Current.Dispatcher.Invoke(action); 13 | 14 | protected override Task InvokeAsync(Action action) => Application.Current.Dispatcher.InvokeAsync(action).Task; 15 | } 16 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | 5 | [assembly: AssemblyTitle("HackF5.UnitySpy.Gui.Wpf")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("HackF5.UnitySpy.Gui.Wpf")] 10 | [assembly: AssemblyCopyright("Copyright © 2019")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: ThemeInfo( 17 | ResourceDictionaryLocation.None, 18 | ResourceDictionaryLocation.SourceAssembly)] 19 | 20 | [assembly: AssemblyVersion("1.0.0.0")] 21 | [assembly: AssemblyFileVersion("1.0.0.0")] 22 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace HackF5.UnitySpy.Gui.Wpf.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HackF5.UnitySpy.Gui.Wpf.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace HackF5.UnitySpy.Gui.Wpf.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/AssemblyImageView.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 | 37 | Types with static fields only 38 | 39 | 48 | 49 | 50 | 51 | 52 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 77 | 78 | 82 | 83 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/AssemblyImageView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.View 2 | { 3 | using System; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Data; 7 | using System.Windows.Threading; 8 | using HackF5.UnitySpy.Gui.Wpf.ViewModel; 9 | 10 | public partial class AssemblyImageView 11 | { 12 | public AssemblyImageView() 13 | { 14 | this.InitializeComponent(); 15 | this.DefinitionsList.DataContextChanged += (sender, args) => 16 | this.Dispatcher.BeginInvoke(new Action(this.RefreshView), DispatcherPriority.Input); 17 | } 18 | 19 | private bool Filter(object item) 20 | { 21 | if (!(item is TypeDefinitionViewModel definition)) 22 | { 23 | return false; 24 | } 25 | 26 | if ((this.HasStaticOnly.IsChecked ?? false) && !definition.HasStaticFields) 27 | { 28 | return false; 29 | } 30 | 31 | var text = this.ListViewFilter.Text.Trim(); 32 | if (string.IsNullOrEmpty(text)) 33 | { 34 | return true; 35 | } 36 | 37 | if (!(definition.FullName.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0)) 38 | { 39 | return false; 40 | } 41 | 42 | return true; 43 | } 44 | 45 | private void HasStaticOnly_OnClick(object sender, RoutedEventArgs e) => this.RefreshView(); 46 | 47 | private void ListViewFilter_OnTextChanged(object sender, TextChangedEventArgs e) => this.RefreshView(); 48 | 49 | private void RefreshView() 50 | { 51 | var source = this.DefinitionsList?.ItemsSource; 52 | if (source == null) 53 | { 54 | return; 55 | } 56 | 57 | var view = CollectionViewSource.GetDefaultView(source); 58 | view.Filter = this.Filter; 59 | view.Refresh(); 60 | } 61 | 62 | private void DefinitionsList_OnSelectionChanged(object sender, SelectionChangedEventArgs e) 63 | { 64 | this.DefinitionsList.ScrollIntoView(this.DefinitionsList.SelectedItem); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/ListContentView.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/ListContentView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.View 2 | { 3 | using System.Windows.Input; 4 | using HackF5.UnitySpy.Gui.ViewModel; 5 | 6 | public partial class ListContentView 7 | { 8 | public ListContentView() 9 | { 10 | this.InitializeComponent(); 11 | } 12 | 13 | private void Control_OnMouseDoubleClick(object sender, MouseButtonEventArgs e) 14 | { 15 | if (!(this.ItemsList.SelectedItem is ListItemViewModel item)) 16 | { 17 | return; 18 | } 19 | 20 | if (!(this.DataContext is ListContentViewModel model)) 21 | { 22 | return; 23 | } 24 | 25 | model.OnAppendToTrail(item.Index.ToString()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/MainView.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 34 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/MainView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.View 2 | { 3 | using System.Windows; 4 | 5 | public partial class MainView : Window 6 | { 7 | public MainView() 8 | { 9 | this.InitializeComponent(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/ManagedObjectInstanceContentView.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 19 | 20 | 21 | 22 | 25 | 26 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/ManagedObjectInstanceContentView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.View 2 | { 3 | using System.Windows.Input; 4 | using HackF5.UnitySpy.Gui.ViewModel; 5 | 6 | public partial class ManagedObjectInstanceContentView 7 | { 8 | public ManagedObjectInstanceContentView() 9 | { 10 | this.InitializeComponent(); 11 | } 12 | 13 | private void Control_OnMouseDoubleClick(object sender, MouseButtonEventArgs e) 14 | { 15 | if (!(this.ItemsList.SelectedItem is InstanceFieldViewModel item)) 16 | { 17 | return; 18 | } 19 | 20 | if (!(this.DataContext is ManagedObjectInstanceContentViewModel model)) 21 | { 22 | return; 23 | } 24 | 25 | model.OnAppendToTrail(item.Name); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/TypeDefinitionContentView.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 20 | 21 | 22 | 23 | 26 | 27 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/TypeDefinitionContentView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.View 2 | { 3 | using System.Windows.Input; 4 | using HackF5.UnitySpy.Gui.ViewModel; 5 | using HackF5.UnitySpy.Gui.Wpf.ViewModel; 6 | 7 | public partial class TypeDefinitionContentView 8 | { 9 | public TypeDefinitionContentView() 10 | { 11 | this.InitializeComponent(); 12 | } 13 | 14 | private void Control_OnMouseDoubleClick(object sender, MouseButtonEventArgs e) 15 | { 16 | if (!(this.ItemsList.SelectedItem is StaticFieldViewModel item)) 17 | { 18 | return; 19 | } 20 | 21 | if (!(this.DataContext is TypeDefinitionContentViewModel model)) 22 | { 23 | return; 24 | } 25 | 26 | model.OnAppendToTrail(item.Name); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/TypeDefinitionView.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 38 | 50 | 51 | 59 | 60 | 61 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/View/TypeDefinitionView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.View 2 | { 3 | public partial class TypeDefinitionView 4 | { 5 | public TypeDefinitionView() 6 | { 7 | this.InitializeComponent(); 8 | this.Loaded += (sender, args) => this.PathTextBox.Focus(); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/ViewModel/AssemblyImageViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.ViewModel 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using HackF5.UnitySpy.Gui.Mvvm; 7 | using HackF5.UnitySpy.Gui.Wpf.Mvvm; 8 | using JetBrains.Annotations; 9 | 10 | public class AssemblyImageViewModel : PropertyChangedBase 11 | { 12 | private readonly IAssemblyImage image; 13 | 14 | private readonly TypeDefinitionViewModel.Factory typeDefinitionFactory; 15 | 16 | private TypeDefinitionViewModel selectedType; 17 | 18 | public AssemblyImageViewModel( 19 | [NotNull] IAssemblyImage image, 20 | [NotNull] TypeDefinitionViewModel.Factory typeDefinitionFactory) 21 | { 22 | this.image = image ?? throw new ArgumentNullException(nameof(image)); 23 | 24 | this.typeDefinitionFactory = 25 | typeDefinitionFactory ?? throw new ArgumentNullException(nameof(typeDefinitionFactory)); 26 | 27 | Task.Run(this.Init); 28 | } 29 | 30 | public delegate AssemblyImageViewModel Factory(IAssemblyImage image); 31 | 32 | public TypeDefinitionViewModel SelectedType 33 | { 34 | get => this.selectedType; 35 | set => this.SetProperty(ref this.selectedType, value); 36 | } 37 | 38 | public BindableCollection Types { get; } = 39 | new BindableCollection(); 40 | 41 | private void Init() 42 | { 43 | this.Types.IsNotifying = false; 44 | this.Types.Clear(); 45 | 46 | foreach (var type in this.image.TypeDefinitions.OrderBy(td => td.FullName) 47 | .Select(td => this.typeDefinitionFactory(td))) 48 | { 49 | this.Types.Add(type); 50 | } 51 | 52 | this.Types.IsNotifying = true; 53 | this.Types.Refresh(); 54 | 55 | this.SelectedType = this.Types.FirstOrDefault(t => t.FullName == "CollectionManager"); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/ViewModel/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.ViewModel 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using HackF5.UnitySpy.Gui.Mvvm; 8 | using HackF5.UnitySpy.Gui.Wpf.Mvvm; 9 | using HackF5.UnitySpy.Offsets; 10 | using HackF5.UnitySpy.ProcessFacade; 11 | 12 | public class MainViewModel : PropertyChangedBase 13 | { 14 | private readonly CommandCollection commands; 15 | 16 | private readonly AssemblyImageViewModel.Factory imageFactory; 17 | 18 | private readonly ProcessViewModel.Factory processFactory; 19 | 20 | private AssemblyImageViewModel image; 21 | 22 | private ProcessViewModel selectedProcess; 23 | 24 | public MainViewModel( 25 | CommandCollection commands, 26 | ProcessViewModel.Factory processFactory, 27 | AssemblyImageViewModel.Factory imageFactory) 28 | { 29 | this.commands = commands; 30 | this.processFactory = processFactory; 31 | this.imageFactory = imageFactory; 32 | this.RefreshProcessesCommand.Execute(default); 33 | } 34 | 35 | public AssemblyImageViewModel Image 36 | { 37 | get => this.image; 38 | set => this.SetProperty(ref this.image, value); 39 | } 40 | 41 | public BindableCollection Processes { get; } = new BindableCollection(); 42 | 43 | public AsyncCommand RefreshProcessesCommand => this.commands.CreateAsyncCommand(this.ExecuteRefreshProcesses); 44 | 45 | public ProcessViewModel SelectedProcess 46 | { 47 | get => this.selectedProcess; 48 | set 49 | { 50 | if (!this.SetProperty(ref this.selectedProcess, value)) 51 | { 52 | return; 53 | } 54 | 55 | Task.Run(() => this.BuildImageAsync(this.SelectedProcess)); 56 | } 57 | } 58 | 59 | private async Task BuildImageAsync(ProcessViewModel process) 60 | { 61 | try 62 | { 63 | var windowsProcessFacade = new ProcessFacadeWindows(process.Process); 64 | var monoLibraryOffsets = MonoLibraryOffsets.GetOffsets(windowsProcessFacade.GetMainModuleFileName()); 65 | var unityProcessFacade = new UnityProcessFacade(windowsProcessFacade, monoLibraryOffsets); 66 | this.Image = this.imageFactory(AssemblyImageFactory.Create(unityProcessFacade)); 67 | } 68 | catch (Exception ex) 69 | { 70 | await DialogService.ShowAsync( 71 | $"Failed to load process {process.Name} ({process.ProcessId}).", 72 | ex.Message); 73 | } 74 | } 75 | 76 | private async Task ExecuteRefreshProcesses() 77 | { 78 | this.Image = default; 79 | var processes = await Task.Run(Process.GetProcesses); 80 | 81 | this.SelectedProcess = default; 82 | this.Processes.IsNotifying = false; 83 | this.Processes.Clear(); 84 | 85 | foreach (var process in processes.OrderBy(p => p.ProcessName).Select(p => this.processFactory(p))) 86 | { 87 | this.Processes.Add(process); 88 | } 89 | 90 | this.Processes.IsNotifying = true; 91 | this.Processes.Refresh(); 92 | 93 | this.SelectedProcess = this.Processes.FirstOrDefault(p => p.Name == "Hearthstone"); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/ViewModel/ProcessViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.ViewModel 2 | { 3 | using System.Diagnostics; 4 | using HackF5.UnitySpy.Gui.Mvvm; 5 | 6 | public class ProcessViewModel : PropertyChangedBase 7 | { 8 | private readonly Process process; 9 | 10 | public ProcessViewModel(Process process) 11 | { 12 | this.process = process; 13 | } 14 | 15 | public delegate ProcessViewModel Factory(Process process); 16 | 17 | public string Name => this.process.ProcessName; 18 | 19 | public Process Process => this.process; 20 | 21 | public int ProcessId => this.process.Id; 22 | } 23 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/ViewModel/TypeDefinitionContentViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Wpf.ViewModel 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using HackF5.UnitySpy.Gui.Mvvm; 7 | using HackF5.UnitySpy.Gui.ViewModel; 8 | using JetBrains.Annotations; 9 | 10 | public class TypeDefinitionContentViewModel : PropertyChangedBase 11 | { 12 | private readonly ITypeDefinition definition; 13 | 14 | public TypeDefinitionContentViewModel( 15 | [NotNull] ITypeDefinition definition, 16 | StaticFieldViewModel.Factory fieldFactory) 17 | { 18 | this.definition = definition ?? throw new ArgumentNullException(nameof(definition)); 19 | this.StaticFields = this.definition.Fields.Where(f => f.TypeInfo.IsStatic && !f.TypeInfo.IsConstant) 20 | .Select(f => fieldFactory(f)) 21 | .ToArray(); 22 | } 23 | 24 | public delegate TypeDefinitionContentViewModel Factory(ITypeDefinition definition); 25 | 26 | public event EventHandler AppendToTrail; 27 | 28 | public IEnumerable StaticFields { get; } 29 | 30 | public virtual void OnAppendToTrail(string value) 31 | { 32 | this.AppendToTrail?.Invoke(this, new AppendToTrailEventArgs(value)); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui.Wpf/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/HackF5.UnitySpy.Gui.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | ..\rules.ruleset 6 | 1701;1702;CA1724;CA1031 7 | latest 8 | true 9 | true 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | 18 | x64 19 | 20 | 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/Mvvm/AutofacExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Mvvm 2 | { 3 | using System; 4 | using System.Reflection; 5 | using Autofac; 6 | using Autofac.Builder; 7 | using Autofac.Util; 8 | using JetBrains.Annotations; 9 | 10 | [PublicAPI] 11 | public static class AutofacExtensions 12 | { 13 | public static IRegistrationBuilder AsSingleton( 14 | this IRegistrationBuilder @this) => 15 | @this.AsImplementedInterfaces().AsSelf().SingleInstance(); 16 | 17 | public static IRegistrationBuilder AsTransient( 18 | this IRegistrationBuilder @this) => 19 | @this.AsImplementedInterfaces().AsSelf().InstancePerDependency(); 20 | 21 | public static void AutoRegisterAssemblyTypes(this ContainerBuilder builder, Assembly assembly) 22 | { 23 | foreach (var type in assembly.GetLoadableTypes()) 24 | { 25 | if (type.GetCustomAttribute() is var attribute && attribute != null) 26 | { 27 | switch (attribute.Type) 28 | { 29 | case RegistrationType.Transient: 30 | builder.RegisterType(type).AsTransient(); 31 | break; 32 | case RegistrationType.Singleton: 33 | builder.RegisterType(type).AsSingleton(); 34 | break; 35 | default: 36 | throw new InvalidOperationException( 37 | $"Unknown registration type {attribute.Type} on type {type}."); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/Mvvm/IMainThreadInvoker.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Mvvm 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | public interface IMainThreadInvoker 7 | { 8 | void InvokeOnMainThread(Action action); 9 | 10 | Task InvokeOnMainThreadAsync(Action action); 11 | } 12 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/Mvvm/MainThreadInvoker.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Mvvm 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using JetBrains.Annotations; 7 | 8 | public abstract class MainThreadInvoker : IMainThreadInvoker 9 | { 10 | public static IMainThreadInvoker Current { get; set; } 11 | 12 | public void InvokeOnMainThread([InstantHandle] [NotNull] Action action) 13 | { 14 | if (action == null) 15 | { 16 | throw new ArgumentNullException(nameof(action)); 17 | } 18 | 19 | if (this.CheckAccess()) 20 | { 21 | action(); 22 | return; 23 | } 24 | 25 | this.Invoke(action); 26 | } 27 | 28 | public async Task InvokeOnMainThreadAsync(Action action) 29 | { 30 | if (action == null) 31 | { 32 | throw new ArgumentNullException(nameof(action)); 33 | } 34 | 35 | if (this.CheckAccess()) 36 | { 37 | action(); 38 | return; 39 | } 40 | 41 | await this.InvokeAsync(action); 42 | } 43 | 44 | protected abstract bool CheckAccess(); 45 | 46 | protected abstract void Invoke(Action action); 47 | 48 | protected abstract Task InvokeAsync(Action action); 49 | } 50 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/Mvvm/PropertyChangedBase.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Mvvm 2 | { 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | using JetBrains.Annotations; 6 | 7 | [PublicAPI] 8 | public class PropertyChangedBase : INotifyPropertyChanged 9 | { 10 | public event PropertyChangedEventHandler PropertyChanged; 11 | 12 | public bool SetProperty(ref T propertyField, T value, [CallerMemberName] string propertyName = null) 13 | { 14 | if (object.Equals(propertyField, value)) 15 | { 16 | return false; 17 | } 18 | 19 | propertyField = value; 20 | this.RaisePropertyChanged(propertyName); 21 | return true; 22 | } 23 | 24 | [NotifyPropertyChangedInvocator] 25 | protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null) 26 | { 27 | var handler = this.PropertyChanged; 28 | if (handler == null) 29 | { 30 | return; 31 | } 32 | 33 | MainThreadInvoker.Current.InvokeOnMainThread( 34 | () => handler(this, new PropertyChangedEventArgs(propertyName))); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/Mvvm/RegisterAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Mvvm 2 | { 3 | using System; 4 | 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public sealed class RegisterAttribute : Attribute 7 | { 8 | public RegisterAttribute(RegistrationType type) 9 | { 10 | this.Type = type; 11 | } 12 | 13 | public RegistrationType Type { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/Mvvm/RegistrationType.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.Mvvm 2 | { 3 | public enum RegistrationType 4 | { 5 | Transient, 6 | Singleton, 7 | } 8 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/ViewModel/AppendToTrailEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.ViewModel 2 | { 3 | using System; 4 | 5 | public class AppendToTrailEventArgs : EventArgs 6 | { 7 | public AppendToTrailEventArgs(string value) 8 | { 9 | this.Value = value; 10 | } 11 | 12 | public string Value { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/ViewModel/InstanceFieldViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.ViewModel 2 | { 3 | using System; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Reflection; 7 | using HackF5.UnitySpy.Gui.Mvvm; 8 | using JetBrains.Annotations; 9 | using TypeCode = HackF5.UnitySpy.Detail.TypeCode; 10 | 11 | public class InstanceFieldViewModel : PropertyChangedBase 12 | { 13 | private readonly IFieldDefinition field; 14 | 15 | private readonly IManagedObjectInstance instance; 16 | 17 | public InstanceFieldViewModel([NotNull] IFieldDefinition field, [NotNull] IManagedObjectInstance instance) 18 | { 19 | this.field = field ?? throw new ArgumentNullException(nameof(field)); 20 | this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); 21 | } 22 | 23 | public delegate InstanceFieldViewModel Factory(IFieldDefinition field, IManagedObjectInstance instance); 24 | 25 | public string Name => this.field.Name; 26 | 27 | public string TypeName 28 | { 29 | get 30 | { 31 | if (this.field.TypeInfo.TryGetTypeDefinition(out var typeDefinition)) 32 | { 33 | return typeDefinition.FullName; 34 | } 35 | 36 | var typeName = this.field.TypeInfo.TypeCode.ToString(); 37 | var member = typeof(TypeCode).GetMember(typeName).FirstOrDefault(); 38 | return member?.GetCustomAttribute()?.Description ?? typeName; 39 | } 40 | } 41 | 42 | public object Value 43 | { 44 | get 45 | { 46 | try 47 | { 48 | return this.instance.GetValue(this.Name); 49 | } 50 | catch (Exception ex) 51 | { 52 | return $"ERROR: {ex.Message}"; 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/ViewModel/ListContentViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.ViewModel 2 | { 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using HackF5.UnitySpy.Gui.Mvvm; 8 | 9 | public class ListContentViewModel : PropertyChangedBase 10 | { 11 | private readonly IList list; 12 | 13 | public ListContentViewModel(IList list, ListItemViewModel.Factory itemFactory) 14 | { 15 | this.list = list; 16 | this.Items = this.list.Cast().Select((o, i) => itemFactory(o, i)).ToArray(); 17 | } 18 | 19 | public delegate ListContentViewModel Factory(IList list); 20 | 21 | public event EventHandler AppendToTrail; 22 | 23 | public IEnumerable Items { get; } 24 | 25 | public virtual void OnAppendToTrail(string value) 26 | { 27 | this.AppendToTrail?.Invoke(this, new AppendToTrailEventArgs(value)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/ViewModel/ListItemViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.ViewModel 2 | { 3 | using HackF5.UnitySpy.Gui.Mvvm; 4 | 5 | public class ListItemViewModel : PropertyChangedBase 6 | { 7 | public ListItemViewModel(object item, int index) 8 | { 9 | this.Item = item; 10 | this.Index = index; 11 | } 12 | 13 | public delegate ListItemViewModel Factory(object item, int index); 14 | 15 | public object Item { get; } 16 | 17 | public int Index { get; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/ViewModel/ManagedObjectInstanceContentViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.ViewModel 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using HackF5.UnitySpy.Gui.Mvvm; 7 | using JetBrains.Annotations; 8 | 9 | public class ManagedObjectInstanceContentViewModel : PropertyChangedBase 10 | { 11 | private readonly IManagedObjectInstance instance; 12 | 13 | public ManagedObjectInstanceContentViewModel( 14 | [NotNull] IManagedObjectInstance instance, 15 | [NotNull] InstanceFieldViewModel.Factory fieldFactory) 16 | { 17 | if (fieldFactory == null) 18 | { 19 | throw new ArgumentNullException(nameof(fieldFactory)); 20 | } 21 | 22 | this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); 23 | this.InstanceFields = this.instance.TypeDefinition.Fields 24 | .Where(f => !f.TypeInfo.IsStatic && !f.TypeInfo.IsConstant) 25 | .Select(f => fieldFactory(f, instance)) 26 | .ToArray(); 27 | } 28 | 29 | public delegate ManagedObjectInstanceContentViewModel Factory(IManagedObjectInstance instance); 30 | 31 | public event EventHandler AppendToTrail; 32 | 33 | public IEnumerable InstanceFields { get; } 34 | 35 | public virtual void OnAppendToTrail(string value) 36 | { 37 | this.AppendToTrail?.Invoke(this, new AppendToTrailEventArgs(value)); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Gui/ViewModel/StaticFieldViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Gui.ViewModel 2 | { 3 | using System; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Reflection; 7 | using JetBrains.Annotations; 8 | using TypeCode = HackF5.UnitySpy.Detail.TypeCode; 9 | 10 | [UsedImplicitly] 11 | public class StaticFieldViewModel 12 | { 13 | private readonly IFieldDefinition field; 14 | 15 | public StaticFieldViewModel([NotNull] IFieldDefinition field) 16 | { 17 | this.field = field ?? throw new ArgumentNullException(nameof(field)); 18 | } 19 | 20 | public delegate StaticFieldViewModel Factory(IFieldDefinition field); 21 | 22 | public string Name => this.field.Name; 23 | 24 | public string TypeName 25 | { 26 | get 27 | { 28 | if (this.field.TypeInfo.TryGetTypeDefinition(out var typeDefinition)) 29 | { 30 | return typeDefinition.FullName; 31 | } 32 | 33 | var typeName = this.field.TypeInfo.TypeCode.ToString(); 34 | var member = typeof(TypeCode).GetMember(typeName).FirstOrDefault(); 35 | return member?.GetCustomAttribute()?.Description ?? typeName; 36 | } 37 | } 38 | 39 | public object Value 40 | { 41 | get 42 | { 43 | try 44 | { 45 | return this.field.DeclaringType.GetStaticValue(this.Name); 46 | } 47 | catch (Exception ex) 48 | { 49 | return $"ERROR: {ex.Message}"; 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Native/Makefile: -------------------------------------------------------------------------------- 1 | linux: 2 | gcc -o server server.c -lpthread 3 | 4 | macos: 5 | gcc -o server server.c -lpthread 6 | 7 | clean: 8 | rm -f server 9 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Native/linux.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "server.h" 6 | 7 | int32_t read_process_memory_to_buffer(int pid, uint64_t addr, char* buffer, uint32_t size) 8 | { 9 | char filepath[50]; 10 | int32_t bytes_to_read = 0; 11 | 12 | //Generates the path to the file to be read 13 | sprintf(filepath, "/proc/%i/mem", pid); 14 | 15 | FILE *fp = fopen(filepath, "r"); 16 | if(fp == NULL) 17 | { 18 | perror("Open file error"); 19 | printf("File: %s, pid as i: %i, pid as u: %u\n", filepath, pid, pid); 20 | } 21 | else 22 | { 23 | fseek(fp, addr, SEEK_SET); 24 | bytes_to_read = fread(buffer, sizeof(char), size, fp); 25 | fclose(fp); 26 | } 27 | 28 | return bytes_to_read; 29 | } 30 | 31 | char* get_module_info(int pid, const char* module_name, int* module_size, char* path) 32 | { 33 | return NULL; 34 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Native/server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include // inet_addr 6 | #include // write 7 | #include // threading, link with lpthread 8 | 9 | #include "server.h" 10 | 11 | #if defined(__linux__) 12 | #include "linux.c" 13 | #elif defined(__MACH__) 14 | #include "macos.c" 15 | #endif 16 | 17 | #define PORT 39185 18 | 19 | #define READ_MEMORY_REQUEST 0 20 | #define GET_MODULE_REQUEST 1 21 | #define REQUEST_SIZE 17 // byte 0 = request type, byte 1-5 = pid, byte 5-13 = address, byte 14-17 = size 22 | 23 | /* 24 | * This will handle connection for each client 25 | * */ 26 | void *connection_handler(void* socket_ptr) 27 | { 28 | int socket = *(int*)socket_ptr; 29 | char buff[BUFFER_SIZE]; 30 | char request[REQUEST_SIZE]; 31 | int32_t* pid = request + 1; 32 | int64_t* address = request + 5; 33 | int32_t* size = request + 13; 34 | 35 | //Receive a request from the client 36 | while(recv(socket, request, REQUEST_SIZE, 0) == REQUEST_SIZE) 37 | { 38 | if (request[0] == GET_MODULE_REQUEST) 39 | { 40 | puts("Requested Module"); 41 | if (recv(socket, buff, *size, 0) < 0) 42 | { 43 | puts("recv module name failed"); 44 | break; 45 | } 46 | else 47 | { 48 | buff[(int)size] = '\0'; 49 | int module_size; 50 | int64_t baseAddress = get_module_info(*pid, buff, &module_size, buff); 51 | 52 | if(send(socket, &baseAddress, sizeof(baseAddress), 0) < 0) 53 | { 54 | perror("Fail to send address module info"); 55 | break; 56 | } 57 | 58 | if(send(socket, &module_size, sizeof(module_size), 0) < 0) 59 | { 60 | perror("Fail to send size module info"); 61 | break; 62 | } 63 | 64 | if(send(socket, buff, strlen(buff), 0) < 0) 65 | { 66 | perror("Fail to send path module info"); 67 | break; 68 | } 69 | } 70 | } 71 | else if (request[0] == READ_MEMORY_REQUEST) 72 | { 73 | uint32_t bytes_to_read; 74 | uint32_t remaining_bytes = *size; 75 | uint64_t address_limit = *address + remaining_bytes; 76 | 77 | // printf("Memory chunk requested: pid = %i, address = %08X, size = %u\n", *pid, *address, remaining_bytes); 78 | 79 | while (remaining_bytes > 0) 80 | { 81 | if(remaining_bytes < BUFFER_SIZE) 82 | { 83 | bytes_to_read = remaining_bytes; 84 | } 85 | else 86 | { 87 | bytes_to_read = BUFFER_SIZE; 88 | } 89 | bytes_to_read = read_process_memory_to_buffer(*pid, address_limit - remaining_bytes, buff, bytes_to_read); 90 | remaining_bytes -= bytes_to_read; 91 | if(send(socket, buff, bytes_to_read, 0) != bytes_to_read) 92 | { 93 | perror("Fail to send file"); 94 | break; 95 | } 96 | } 97 | } 98 | else 99 | { 100 | printf("Request not recognized %u\n", request[0]); 101 | } 102 | } 103 | 104 | puts("Closing connection"); 105 | close(socket); 106 | fflush(stdout); 107 | } 108 | 109 | int main(int argc, char *argv[]) 110 | { 111 | int socket_desc, new_socket, c; 112 | struct sockaddr_in server, client; 113 | pthread_t thread_id; 114 | 115 | //Create socket 116 | socket_desc = socket(AF_INET, SOCK_STREAM, 0); 117 | if (socket_desc == -1) 118 | { 119 | printf("Could not create socket"); 120 | } 121 | 122 | //Prepare the sockaddr_in structure 123 | server.sin_family = AF_INET; 124 | server.sin_addr.s_addr = INADDR_ANY; 125 | server.sin_port = htons( PORT ); 126 | 127 | //Bind 128 | if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0) 129 | { 130 | puts("bind failed"); 131 | return 1; 132 | } 133 | puts("bind done"); 134 | 135 | //Listen 136 | listen(socket_desc, 3); 137 | 138 | //Accept and incoming connection 139 | puts("Waiting for incoming connections..."); 140 | c = sizeof(struct sockaddr_in); 141 | while( (new_socket = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c)) ) 142 | { 143 | if( pthread_create( &thread_id , NULL , connection_handler , (void*) &new_socket) < 0) 144 | { 145 | perror("could not create thread"); 146 | return 1; 147 | } 148 | } 149 | 150 | puts("Exiting..."); 151 | fflush(stdout); 152 | 153 | if (new_socket < 0) 154 | { 155 | perror("accept failed"); 156 | return 1; 157 | } 158 | 159 | return 0; 160 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Native/server.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_H 2 | #define SERVER_H 3 | 4 | #include 5 | #include 6 | 7 | #define BUFFER_SIZE 4096 8 | 9 | int str_ends_with(const char *str, const char *suffix) 10 | { 11 | if (!str || !suffix) 12 | return 0; 13 | size_t lenstr = strlen(str); 14 | size_t lensuffix = strlen(suffix); 15 | if (lensuffix > lenstr) 16 | return 0; 17 | return strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0; 18 | } 19 | 20 | #endif -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Tests/HackF5.UnitySpy.HearthstoneLib.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0 5 | ..\rules.ruleset 6 | 1701;1702;CA1724;CA1031 7 | latest 8 | true 9 | ..\HackF5.UnitySpy.snk 10 | AnyCPU 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.Tests/TestHearthstoneLibApi.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.HearthstoneLib.Tests 2 | { 3 | using JetBrains.Annotations; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | // This needs to be run while Hearthstone is running 7 | [TestClass] 8 | public class TestHearthstoneLibApi 9 | { 10 | [PublicAPI] 11 | public TestContext TestContext { get; set; } 12 | 13 | [TestMethod] 14 | public void TestRetrieveCollection() 15 | { 16 | var collection = new MindVision().GetCollectionCards(); 17 | Assert.IsNotNull(collection); 18 | Assert.IsTrue(collection.Count > 0, "Collection should not be empty."); 19 | this.TestContext.WriteLine($"Collection has {collection.Count} cards."); 20 | } 21 | 22 | // You need to have a game running for this 23 | [TestMethod] 24 | public void TestRetrieveMatchInfo() 25 | { 26 | var matchInfo = new MindVision().GetMatchInfo(); 27 | Assert.IsNotNull(matchInfo); 28 | this.TestContext.WriteLine($"Local player's standard rank is {matchInfo.LocalPlayer.StandardRank}."); 29 | } 30 | 31 | // You need to have a solo run (Dungeon Run, Monster Hunt, Rumble Run, Dalaran Heist, Tombs of Terror) ongoing 32 | [TestMethod] 33 | public void TestRetrieveFullDungeonInfo() 34 | { 35 | var dungeonInfo = new MindVision().GetDungeonInfoCollection(); 36 | Assert.IsNotNull(dungeonInfo); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HackF5.UnitySpy", "HackF5.UnitySpy\HackF5.UnitySpy.csproj", "{7207C1B6-4688-418E-A590-380C9488260F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".shared", ".shared", "{DD3559B1-4C0B-4EB9-82DB-DB0E82FDEC64}" 9 | ProjectSection(SolutionItems) = preProject 10 | Directory.Build.props = Directory.Build.props 11 | HackF5.UnitySpy.sln.DotSettings = HackF5.UnitySpy.sln.DotSettings 12 | HackF5.UnitySpy.snk = HackF5.UnitySpy.snk 13 | ..\README.md = ..\README.md 14 | rules.ruleset = rules.ruleset 15 | EndProjectSection 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HackF5.UnitySpy.Gui.Wpf", "HackF5.UnitySpy.Gui.Wpf\HackF5.UnitySpy.Gui.Wpf.csproj", "{53214F8D-8702-490D-930D-6DA45DBEBD85}" 18 | ProjectSection(ProjectDependencies) = postProject 19 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01} = {B1664CDB-2F21-46D6-9A63-D28A6916BA01} 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HackF5.UnitySpy.Gui", "HackF5.UnitySpy.Gui\HackF5.UnitySpy.Gui.csproj", "{B1664CDB-2F21-46D6-9A63-D28A6916BA01}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HackF5.UnitySpy.Gui.Avalonia", "HackF5.UnitySpy.Gui.Avalonia\HackF5.UnitySpy.Gui.Avalonia.csproj", "{3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Debug|x64 = Debug|x64 30 | Debug|x86 = Debug|x86 31 | Release|Any CPU = Release|Any CPU 32 | Release|x64 = Release|x64 33 | Release|x86 = Release|x86 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {7207C1B6-4688-418E-A590-380C9488260F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {7207C1B6-4688-418E-A590-380C9488260F}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {7207C1B6-4688-418E-A590-380C9488260F}.Debug|x64.ActiveCfg = Debug|Any CPU 39 | {7207C1B6-4688-418E-A590-380C9488260F}.Debug|x64.Build.0 = Debug|Any CPU 40 | {7207C1B6-4688-418E-A590-380C9488260F}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {7207C1B6-4688-418E-A590-380C9488260F}.Debug|x86.Build.0 = Debug|Any CPU 42 | {7207C1B6-4688-418E-A590-380C9488260F}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {7207C1B6-4688-418E-A590-380C9488260F}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {7207C1B6-4688-418E-A590-380C9488260F}.Release|x64.ActiveCfg = Release|Any CPU 45 | {7207C1B6-4688-418E-A590-380C9488260F}.Release|x64.Build.0 = Release|Any CPU 46 | {7207C1B6-4688-418E-A590-380C9488260F}.Release|x86.ActiveCfg = Release|Any CPU 47 | {7207C1B6-4688-418E-A590-380C9488260F}.Release|x86.Build.0 = Release|Any CPU 48 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Debug|x64.ActiveCfg = Debug|x64 51 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Debug|x64.Build.0 = Debug|x64 52 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Debug|x86.ActiveCfg = Debug|x86 53 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Debug|x86.Build.0 = Debug|x86 54 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Release|x64.ActiveCfg = Release|x64 57 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Release|x64.Build.0 = Release|x64 58 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Release|x86.ActiveCfg = Release|x86 59 | {53214F8D-8702-490D-930D-6DA45DBEBD85}.Release|x86.Build.0 = Release|x86 60 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Debug|x64.ActiveCfg = Debug|Any CPU 63 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Debug|x64.Build.0 = Debug|Any CPU 64 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Debug|x86.ActiveCfg = Debug|Any CPU 65 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Debug|x86.Build.0 = Debug|Any CPU 66 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Release|x64.ActiveCfg = Release|Any CPU 69 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Release|x64.Build.0 = Release|Any CPU 70 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Release|x86.ActiveCfg = Release|Any CPU 71 | {B1664CDB-2F21-46D6-9A63-D28A6916BA01}.Release|x86.Build.0 = Release|Any CPU 72 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Debug|Any CPU.ActiveCfg = Debug|x64 73 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Debug|x64.ActiveCfg = Debug|x64 74 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Debug|x64.Build.0 = Debug|x64 75 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Debug|x86.ActiveCfg = Debug|x64 76 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Release|Any CPU.ActiveCfg = Release|x64 77 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Release|x64.ActiveCfg = Release|x64 78 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Release|x64.Build.0 = Release|x64 79 | {3E9BD1B6-8F86-46CB-8924-7D0F16F57A64}.Release|x86.ActiveCfg = Release|x64 80 | EndGlobalSection 81 | GlobalSection(SolutionProperties) = preSolution 82 | HideSolutionNode = FALSE 83 | EndGlobalSection 84 | GlobalSection(ExtensibilityGlobals) = postSolution 85 | SolutionGuid = {FF2C925F-01B9-49D1-AC1E-11A433147361} 86 | EndGlobalSection 87 | EndGlobal 88 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackf5/unityspy/56227cbe9b6654126f48499929a560c1c9238c1f/src/HackF5.UnitySpy.snk -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/AssemblyImage.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using HackF5.UnitySpy.ProcessFacade; 8 | using JetBrains.Annotations; 9 | 10 | /// 11 | /// Represents an unmanaged _MonoImage instance in a Mono process. This object describes a managed assembly. 12 | /// The .NET equivalent is . 13 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 14 | /// 15 | [PublicAPI] 16 | public class AssemblyImage : MemoryObject, IAssemblyImage 17 | { 18 | private readonly Dictionary typeDefinitionsByFullName = 19 | new Dictionary(); 20 | 21 | private readonly ConcurrentDictionary typeDefinitionsByAddress; 22 | 23 | public AssemblyImage(UnityProcessFacade process, IntPtr address) 24 | : base(null, address) 25 | { 26 | this.Process = process; 27 | 28 | this.typeDefinitionsByAddress = this.CreateTypeDefinitions(); 29 | 30 | foreach (var definition in this.TypeDefinitions) 31 | { 32 | definition.Init(); 33 | } 34 | 35 | foreach (var definition in this.TypeDefinitions) 36 | { 37 | if (definition.FullName.Contains("`")) 38 | { 39 | // ignore generic classes as they have name clashes. in order to make them unique these it would be 40 | // necessary to examine the information held in TypeInfo.Data. see 41 | // ProcessFacade.ReadManagedGenericObject for moral support. 42 | continue; 43 | } 44 | 45 | if (!this.typeDefinitionsByFullName.ContainsKey(definition.FullName)) 46 | { 47 | this.typeDefinitionsByFullName.Add(definition.FullName, definition); 48 | } 49 | } 50 | } 51 | 52 | IEnumerable IAssemblyImage.TypeDefinitions => this.TypeDefinitions; 53 | 54 | public IEnumerable TypeDefinitions => 55 | this.typeDefinitionsByAddress.ToArray().Select(k => k.Value); 56 | 57 | public override AssemblyImage Image => this; 58 | 59 | public override UnityProcessFacade Process { get; } 60 | 61 | public dynamic this[string fullTypeName] => this.GetTypeDefinition(fullTypeName); 62 | 63 | ITypeDefinition IAssemblyImage.GetTypeDefinition(string fullTypeName) => this.GetTypeDefinition(fullTypeName); 64 | 65 | public TypeDefinition GetTypeDefinition(string fullTypeName) => 66 | this.typeDefinitionsByFullName.TryGetValue(fullTypeName, out var d) ? d : default; 67 | 68 | public TypeDefinition GetTypeDefinition(IntPtr address) 69 | { 70 | if (address == IntPtr.Zero) 71 | { 72 | return default; 73 | } 74 | 75 | return this.typeDefinitionsByAddress.GetOrAdd( 76 | address, 77 | key => new TypeDefinition(this, key)); 78 | } 79 | 80 | private ConcurrentDictionary CreateTypeDefinitions() 81 | { 82 | var definitions = new ConcurrentDictionary(); 83 | int classCache = this.Process.MonoLibraryOffsets.ImageClassCache; 84 | var classCacheSize = this.ReadUInt32(classCache + this.Process.MonoLibraryOffsets.HashTableSize); 85 | var classCacheTableArray = this.ReadPtr(classCache + this.Process.MonoLibraryOffsets.HashTableTable); 86 | 87 | for (var tableItem = 0; 88 | tableItem < (classCacheSize * this.Process.SizeOfPtr); 89 | tableItem += this.Process.SizeOfPtr) 90 | { 91 | for (var definition = this.Process.ReadPtr(classCacheTableArray + tableItem); 92 | definition != IntPtr.Zero; 93 | definition = this.Process.ReadPtr(definition + this.Process.MonoLibraryOffsets.TypeDefinitionNextClassCache)) 94 | { 95 | definitions.GetOrAdd(definition, new TypeDefinition(this, definition)); 96 | } 97 | } 98 | 99 | return definitions; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/FieldDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using JetBrains.Annotations; 7 | 8 | /// 9 | /// Represents an unmanaged _MonoClassField instance in a Mono process. This object describes a field in a 10 | /// managed class or struct. The .NET equivalent is . 11 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/class-internals.h. 12 | /// 13 | [PublicAPI] 14 | [DebuggerDisplay( 15 | "Field: {" + nameof(FieldDefinition.Offset) + "} - {" + nameof(FieldDefinition.Name) + "}")] 16 | public class FieldDefinition : MemoryObject, IFieldDefinition 17 | { 18 | private readonly List genericTypeArguments; 19 | 20 | public FieldDefinition([NotNull] TypeDefinition declaringType, IntPtr address) 21 | : base((declaringType ?? throw new ArgumentNullException(nameof(declaringType))).Image, address) 22 | { 23 | this.DeclaringType = declaringType; 24 | 25 | // MonoType *type; 26 | this.TypeInfo = new TypeInfo(declaringType.Image, this.ReadPtr(0x0)); 27 | 28 | // MonoType *name; 29 | this.Name = this.ReadString(this.Process.SizeOfPtr); 30 | 31 | // wee need to skip MonoClass *parent field so we add 32 | // 3 pointer sizes (*type, *name, *parent) to the base address 33 | this.Offset = this.ReadInt32(this.Process.SizeOfPtr * 3); 34 | 35 | // Get the generic type arguments 36 | if (this.TypeInfo.TypeCode == TypeCode.GENERICINST) 37 | { 38 | var monoGenericClassAddress = this.TypeInfo.Data; 39 | var monoClassAddress = this.Process.ReadPtr(monoGenericClassAddress); 40 | this.Image.GetTypeDefinition(monoClassAddress); 41 | 42 | var monoGenericContainerPtr = monoClassAddress + this.Process.MonoLibraryOffsets.TypeDefinitionGenericContainer; 43 | var monoGenericContainerAddress = this.Process.ReadPtr(monoGenericContainerPtr); 44 | 45 | var monoGenericContextPtr = monoGenericClassAddress + this.Process.SizeOfPtr; 46 | var monoGenericInsPtr = this.Process.ReadPtr(monoGenericContextPtr); 47 | 48 | // var argumentCount = this.Process.ReadInt32(monoGenericInsPtr + 0x4); 49 | var argumentCount = this.Process.ReadInt32(monoGenericContainerAddress + (4 * this.Process.SizeOfPtr)); 50 | var typeArgVPtr = monoGenericInsPtr + 0x8; 51 | this.genericTypeArguments = new List(argumentCount); 52 | for (int i = 0; i < argumentCount; i++) 53 | { 54 | var genericTypeArgumentPtr = this.Process.ReadPtr(typeArgVPtr + (i * this.Process.SizeOfPtr)); 55 | this.genericTypeArguments.Add(new TypeInfo(this.Image, genericTypeArgumentPtr)); 56 | } 57 | } 58 | else 59 | { 60 | this.genericTypeArguments = null; 61 | } 62 | } 63 | 64 | ITypeDefinition IFieldDefinition.DeclaringType => this.DeclaringType; 65 | 66 | public string Name { get; } 67 | 68 | ITypeInfo IFieldDefinition.TypeInfo => this.TypeInfo; 69 | 70 | public TypeDefinition DeclaringType { get; } 71 | 72 | public int Offset { get; set; } 73 | 74 | public TypeInfo TypeInfo { get; } 75 | 76 | public TValue GetValue(IntPtr address) 77 | { 78 | return this.GetValue(this.genericTypeArguments, address); 79 | } 80 | 81 | public TValue GetValue(List genericTypeArguments, IntPtr address) 82 | { 83 | int offset = this.DeclaringType.IsValueType && !this.TypeInfo.IsStatic ? this.Offset - (this.Process.SizeOfPtr * 2) : this.Offset; 84 | return (TValue)this.TypeInfo.GetValue(this.genericTypeArguments ?? genericTypeArguments, address + offset); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/ManagedClassInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// Represents a class instance in managed memory. 9 | /// Mono and .NET don't necessarily use the same layout scheme, but assuming it is similar this article provides 10 | /// some useful information: 11 | /// https://web.archive.org/web/20080919091745/http://msdn.microsoft.com:80/en-us/magazine/cc163791.aspx. 12 | /// 13 | [PublicAPI] 14 | public class ManagedClassInstance : ManagedObjectInstance 15 | { 16 | private readonly IntPtr definitionAddress; 17 | 18 | private readonly IntPtr vtable; 19 | 20 | public ManagedClassInstance([NotNull] AssemblyImage image, List genericTypeArguments, IntPtr address) 21 | : base(image, genericTypeArguments, address) 22 | { 23 | if (image == null) 24 | { 25 | throw new ArgumentNullException(nameof(image)); 26 | } 27 | 28 | // the address of the class instance points directly back the the classes VTable 29 | this.vtable = this.ReadPtr(0x0); 30 | 31 | // The VTable points to the class definition itself. 32 | this.definitionAddress = image.Process.ReadPtr(this.vtable); 33 | } 34 | 35 | public override TypeDefinition TypeDefinition => this.Image.GetTypeDefinition(this.definitionAddress); 36 | } 37 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/ManagedObjectInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | public abstract class ManagedObjectInstance : MemoryObject, IManagedObjectInstance 7 | { 8 | private readonly List genericTypeArguments; 9 | 10 | protected ManagedObjectInstance(AssemblyImage image, List genericTypeArguments, IntPtr address) 11 | : base(image, address) 12 | { 13 | this.genericTypeArguments = genericTypeArguments; 14 | } 15 | 16 | ITypeDefinition IManagedObjectInstance.TypeDefinition => this.TypeDefinition; 17 | 18 | public abstract TypeDefinition TypeDefinition { get; } 19 | 20 | public dynamic this[string fieldName] => this.GetValue(fieldName); 21 | 22 | public dynamic this[string fieldName, string typeFullName] => this.GetValue(fieldName, typeFullName); 23 | 24 | public TValue GetValue(string fieldName) => this.GetValue(fieldName, default); 25 | 26 | public TValue GetValue(string fieldName, string typeFullName) 27 | { 28 | var field = this.TypeDefinition.GetField(fieldName, typeFullName) 29 | ?? throw new ArgumentException( 30 | $"No field exists with name {fieldName} in type {typeFullName ?? ""}."); 31 | 32 | return field.GetValue(this.genericTypeArguments, this.Address); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/ManagedStructInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// This class represents a value type /struct instance in managed memory. 9 | /// Mono and .NET don't necessarily use the same layout scheme, but assuming it is similar this article provides 10 | /// some useful information: 11 | /// https://web.archive.org/web/20080919091745/http://msdn.microsoft.com:80/en-us/magazine/cc163791.aspx. 12 | /// 13 | [PublicAPI] 14 | public class ManagedStructInstance : ManagedObjectInstance 15 | { 16 | public ManagedStructInstance([NotNull] TypeDefinition typeDefinition, List genericTypeArguments, IntPtr address) 17 | : base((typeDefinition ?? throw new ArgumentNullException(nameof(typeDefinition))).Image, genericTypeArguments, address) 18 | { 19 | // value type pointers contain no type information as a significant performance optimization. in memory 20 | // a value type is simply a contiguous sequence of bytes and it is up to the runtime to know how to 21 | // interpret those bytes. if you take the example of a integer, then it makes sense why this is 22 | // as if an integer needed an extra pointer that pointed back to the System.Int32 class then the size 23 | // of each one would be doubled. presumably interoperability with native assemblies would also be 24 | // completely scuppered. 25 | this.TypeDefinition = typeDefinition; 26 | } 27 | 28 | public override TypeDefinition TypeDefinition { get; } 29 | } 30 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/MemoryObject.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using HackF5.UnitySpy.ProcessFacade; 5 | 6 | /// 7 | /// The base type for all objects accessed in a process' memory. Every object has an address in memory 8 | /// and all information about that object is accessed via an offset from that address. 9 | /// 10 | public abstract class MemoryObject : IMemoryObject 11 | { 12 | protected MemoryObject(AssemblyImage image, IntPtr address) 13 | { 14 | this.Image = image; 15 | this.Address = address; 16 | } 17 | 18 | IAssemblyImage IMemoryObject.Image => this.Image; 19 | 20 | public virtual AssemblyImage Image { get; } 21 | 22 | public virtual UnityProcessFacade Process => this.Image.Process; 23 | 24 | public IntPtr Address { get; } 25 | 26 | protected int ReadInt32(int offset) => this.Process.ReadInt32(this.Address + offset); 27 | 28 | protected IntPtr ReadPtr(int offset) => this.Process.ReadPtr(this.Address + offset); 29 | 30 | protected string ReadString(int offset) => this.Process.ReadAsciiStringPtr(this.Address + offset); 31 | 32 | protected uint ReadUInt32(int offset) => this.Process.ReadUInt32(this.Address + offset); 33 | 34 | protected byte ReadByte(int offset) => this.Process.ReadByte(this.Address + offset); 35 | } 36 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/MonoClassKind.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | public enum MonoClassKind 4 | { 5 | Def = 1, 6 | GTg = 2, 7 | GInst = 3, 8 | GParam = 4, 9 | Array = 5, 10 | Pointer = 6, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/TypeCode.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace HackF5.UnitySpy.Detail 3 | { 4 | using System.ComponentModel; 5 | 6 | /// 7 | /// Represents the type of an object in managed memory. 8 | /// See: MonoTypeEnum in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/blob.h. 9 | /// 10 | public enum TypeCode 11 | { 12 | END = 0x00, /* End of List */ 13 | VOID = 0x01, 14 | 15 | [Description("bool")] 16 | BOOLEAN = 0x02, 17 | 18 | [Description("char")] 19 | CHAR = 0x03, 20 | 21 | [Description("byte")] 22 | I1 = 0x04, 23 | 24 | [Description("sbyte")] 25 | U1 = 0x05, 26 | 27 | [Description("short")] 28 | I2 = 0x06, 29 | 30 | [Description("ushort")] 31 | U2 = 0x07, 32 | 33 | [Description("int")] 34 | I4 = 0x08, 35 | 36 | [Description("uint")] 37 | U4 = 0x09, 38 | 39 | [Description("long")] 40 | I8 = 0x0a, 41 | 42 | [Description("ulong")] 43 | U8 = 0x0b, 44 | 45 | [Description("float")] 46 | R4 = 0x0c, 47 | 48 | [Description("double")] 49 | R8 = 0x0d, 50 | 51 | [Description("string")] 52 | STRING = 0x0e, 53 | 54 | PTR = 0x0f, /* arg: token */ 55 | BYREF = 0x10, /* arg: token */ 56 | VALUETYPE = 0x11, /* arg: token */ 57 | CLASS = 0x12, /* arg: token */ 58 | VAR = 0x13, /* number */ 59 | ARRAY = 0x14, /* type, rank, boundsCount, bound1, loCount, lo1 */ 60 | GENERICINST = 0x15, /* \x{2026} */ 61 | TYPEDBYREF = 0x16, 62 | 63 | [Description("int")] 64 | I = 0x18, 65 | 66 | [Description("uint")] 67 | U = 0x19, 68 | 69 | FNPTR = 0x1b, /* arg: full method signature */ 70 | OBJECT = 0x1c, 71 | SZARRAY = 0x1d, /* 0-based one-dim-array */ 72 | MVAR = 0x1e, /* number */ 73 | CMOD_REQD = 0x1f, /* arg: typedef or typeref token */ 74 | CMOD_OPT = 0x20, /* optional arg: typedef or typref token */ 75 | INTERNAL = 0x21, /* CLR internal type */ 76 | MODIFIER = 0x40, /* Or with the following types */ 77 | SENTINEL = 0x41, /* Sentinel for varargs method signature */ 78 | PINNED = 0x45, /* Local var that points to pinned object */ 79 | ENUM = 0x55 /* an enumeration */ 80 | } 81 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/TypeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// Represents an unmanaged _MonoType instance in a Mono process. This object describes type information. 9 | /// There is no direct .NET equivalent, but probably comes under the umbrella of . 10 | /// See: _MonoType in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 11 | /// 12 | [PublicAPI] 13 | public class TypeInfo : MemoryObject, ITypeInfo 14 | { 15 | public TypeInfo(AssemblyImage image, IntPtr address) 16 | : base(image, address) 17 | { 18 | this.Data = this.ReadPtr(0x0); 19 | this.Attrs = this.ReadUInt32(this.Process.SizeOfPtr); 20 | } 21 | 22 | public uint Attrs { get; } 23 | 24 | public IntPtr Data { get; } 25 | 26 | public bool IsStatic => (this.Attrs & 0x10) == 0x10; 27 | 28 | public bool IsConstant => (this.Attrs & 0x40) == 0x40; 29 | 30 | public TypeCode TypeCode => (TypeCode)(0xff & (this.Attrs >> 16)); 31 | 32 | public bool TryGetTypeDefinition(out ITypeDefinition typeDefinition) 33 | { 34 | switch (this.TypeCode) 35 | { 36 | case TypeCode.CLASS: 37 | case TypeCode.SZARRAY: 38 | case TypeCode.GENERICINST: 39 | case TypeCode.VALUETYPE: 40 | typeDefinition = this.Image.GetTypeDefinition(this.Process.ReadPtr(this.Data)); 41 | return true; 42 | default: 43 | typeDefinition = null; 44 | return false; 45 | } 46 | } 47 | 48 | public object GetValue(List genericTypeArguments, IntPtr address) 49 | => this.Process.ReadManaged(this, genericTypeArguments, address); 50 | } 51 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/HackF5.UnitySpy.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | ..\rules.ruleset 6 | 1701;1702;CA1724;CA1031 7 | latest 8 | true 9 | ..\HackF5.UnitySpy.snk 10 | AnyCPU 11 | true 12 | 13 | 14 | 15 | true 16 | 17 | 18 | 19 | 20 | x64 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers 28 | 29 | 30 | 31 | all 32 | runtime; build; native; contentfiles; analyzers 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IAssemblyImage.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using System.Collections.Generic; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// Represents an unmanaged _MonoImage instance in a Mono process. This object describes a managed assembly. 8 | /// The .NET equivalent is . 9 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 10 | /// 11 | [PublicAPI] 12 | public interface IAssemblyImage : IMemoryObject 13 | { 14 | /// 15 | /// Gets the type definitions that are referenced by the assembly. So for example, although 16 | /// is not declared in the assembly, since all types inherit from this type, it is 17 | /// available in this collection. 18 | /// 19 | IEnumerable TypeDefinitions { get; } 20 | 21 | dynamic this[string fullTypeName] { get; } 22 | 23 | /// 24 | /// Gets the with given from the assembly image. 25 | /// 26 | /// The full name of the definition including namespace. For example 'System.Object' 27 | /// not 'object'. 28 | /// 29 | /// The with given from the assembly image, or 30 | /// null if no such definition exists. 31 | /// 32 | ITypeDefinition GetTypeDefinition(string fullName); 33 | } 34 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IFieldDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using JetBrains.Annotations; 4 | 5 | /// 6 | /// Represents an unmanaged _MonoClassField instance in a Mono process. This object describes a field in a 7 | /// managed class or struct. The .NET equivalent is . 8 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/class-internals.h. 9 | /// 10 | [PublicAPI] 11 | public interface IFieldDefinition : IMemoryObject 12 | { 13 | /// 14 | /// Gets the name of the field. 15 | /// 16 | string Name { get; } 17 | 18 | /// 19 | /// Gets the of the type in which the field is declared. 20 | /// 21 | ITypeDefinition DeclaringType { get; } 22 | 23 | /// 24 | /// Gets an object that describes type information about field. 25 | /// 26 | ITypeInfo TypeInfo { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IManagedObjectInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using JetBrains.Annotations; 4 | 5 | /// 6 | /// Represents an object instance in managed memory. 7 | /// 8 | [PublicAPI] 9 | public interface IManagedObjectInstance : IMemoryObject 10 | { 11 | /// 12 | /// Gets the that describes the type of this instance. 13 | /// 14 | ITypeDefinition TypeDefinition { get; } 15 | 16 | dynamic this[string fieldName] { get; } 17 | 18 | dynamic this[string fieldName, string typeFullName] { get; } 19 | 20 | /// 21 | /// Gets the value of the field in the instance with the given . 22 | /// 23 | /// 24 | /// The type of the value to get. If unsure of the type you can always use . 25 | /// 26 | /// 27 | /// The name of the field. 28 | /// 29 | /// 30 | /// The value of the field in the instance with the given . 31 | /// 32 | TValue GetValue(string fieldName); 33 | 34 | /// 35 | /// Gets the value of the field in the instance with the given that is declared 36 | /// in type with given . 37 | /// 38 | /// 39 | /// The type of the value to get. If unsure of the type you can always use . 40 | /// 41 | /// 42 | /// The name of the field. 43 | /// 44 | /// 45 | /// The name of the type in which the field is declared. In most cases this can be left null, however 46 | /// to access a private field in a subclass which has the same name as a field declared higher up in the 47 | /// hierarchy it is necessary to provide the full name of the declaring type. 48 | /// 49 | /// 50 | /// The value of the field in the instance with the given . 51 | /// 52 | TValue GetValue(string fieldName, string typeFullName); 53 | } 54 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IMemoryObject.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using JetBrains.Annotations; 4 | 5 | /// 6 | /// Represents an object in a process' memory. 7 | /// 8 | [PublicAPI] 9 | public interface IMemoryObject 10 | { 11 | /// 12 | /// Gets the to which the object belongs. 13 | /// 14 | IAssemblyImage Image { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ITypeDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using System.Collections.Generic; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// Represents an unmanaged _MonoClass instance in a Mono process. This object describes the type of a class or 8 | /// struct. The .NET equivalent is . 9 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/class-internals.h. 10 | /// 11 | [PublicAPI] 12 | public interface ITypeDefinition : IMemoryObject 13 | { 14 | /// 15 | /// Gets the collection of the in the type and all of its base types. 16 | /// 17 | IReadOnlyList Fields { get; } 18 | 19 | /// 20 | /// Gets the full name of the type, for example 'System.Object'. 21 | /// 22 | string FullName { get; } 23 | 24 | /// 25 | /// Gets a value indicating whether the type derives from . 26 | /// 27 | bool IsEnum { get; } 28 | 29 | /// 30 | /// Gets a value indicating whether the type derives from . 31 | /// 32 | bool IsValueType { get; } 33 | 34 | /// 35 | /// Gets the name of the type. For example for 'System.Object' this value would be 'Object'. 36 | /// 37 | string Name { get; } 38 | 39 | /// 40 | /// Gets the namespace of the type. For example for 'System.Object' this value would be 'System'. 41 | /// 42 | string NamespaceName { get; } 43 | 44 | /// 45 | /// Gets an object that describes further information about the type. 46 | /// 47 | ITypeInfo TypeInfo { get; } 48 | 49 | dynamic this[string fieldName] { get; } 50 | 51 | /// 52 | /// Gets the declared in the type with the given . 53 | /// 54 | /// 55 | /// The name of the field. 56 | /// 57 | /// 58 | /// The name of the type in which the field is declared. In most cases this can be left null, however 59 | /// to access a private field in a subclass which has the same name as a field declared higher up in the 60 | /// hierarchy it is necessary to provide the full name of the declaring type. 61 | /// 62 | /// 63 | /// The declared in the type with the given . 64 | /// 65 | IFieldDefinition GetField(string fieldName, string typeFullName = default); 66 | 67 | /// 68 | /// Gets the value of a static field declared in the type. 69 | /// 70 | /// 71 | /// The type of the value to get. If unsure of the type you can always use . 72 | /// 73 | /// 74 | /// The name of the static field. 75 | /// 76 | /// 77 | /// The value of the static field. 78 | /// 79 | TValue GetStaticValue(string fieldName); 80 | } 81 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ITypeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using HackF5.UnitySpy.Detail; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// Represents an unmanaged _MonoType instance in a Mono process. This object describes type information. 8 | /// There is no direct .NET equivalent, but probably comes under the umbrella of . 9 | /// See: _MonoType in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 10 | /// 11 | [PublicAPI] 12 | public interface ITypeInfo 13 | { 14 | /// 15 | /// Gets a value indicating whether the entity the info refers to is static; i.e. a static field or a static 16 | /// class. 17 | /// 18 | bool IsStatic { get; } 19 | 20 | /// 21 | /// Gets a value indicating whether the field the info refers to is a constant. 22 | /// 23 | bool IsConstant { get; } 24 | 25 | /// 26 | /// Gets a value that describes the type of the entity. 27 | /// 28 | TypeCode TypeCode { get; } 29 | 30 | /// 31 | /// Tries to get the that this type info refers to. If the return value is 32 | /// false then refer to information in the . 33 | /// 34 | /// 35 | /// The that this type info refers to. 36 | /// 37 | /// 38 | /// A value indicating success. 39 | /// 40 | bool TryGetTypeDefinition(out ITypeDefinition typeDefinition); 41 | } 42 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/BinaryFormat.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Offsets 2 | { 3 | public enum BinaryFormat 4 | { 5 | PE, 6 | MachO, 7 | } 8 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/MachOFormatOffsets.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable IdentifierTypo 2 | namespace HackF5.UnitySpy.Offsets 3 | { 4 | public static class MachOFormatOffsets 5 | { 6 | // offsets taken from https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html 7 | public const int NumberOfCommands = 0x10; 8 | 9 | public const int LoadCommands = 0x20; 10 | 11 | public const int CommandSize = 0x04; 12 | 13 | public const int SymbolTableOffset = 0x08; 14 | 15 | public const int NumberOfSymbols = 0x0c; 16 | 17 | public const int StringTableOffset = 0x10; 18 | 19 | public const int NListValue = 0x08; 20 | 21 | public const int SizeOfNListItem = 0x10; 22 | } 23 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/PEFormatOffsets.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable IdentifierTypo 2 | namespace HackF5.UnitySpy.Offsets 3 | { 4 | public static class PEFormatOffsets 5 | { 6 | // offsets taken from https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format 7 | public const int Signature = 0x3c; 8 | 9 | // 32 bits 10 | public const int ExportDirectoryIndexPE = 0x78; 11 | 12 | // 64 bits 13 | public const int ExportDirectoryIndexPE32Plus = 0x88; 14 | 15 | public const int NumberOfFunctions = 0x14; 16 | 17 | public const int FunctionAddressArrayIndex = 0x1c; 18 | 19 | public const int FunctionNameArrayIndex = 0x20; 20 | 21 | public const int FunctionEntrySize = 4; 22 | 23 | public static int GetExportDirectoryIndex(bool is64Bits) 24 | { 25 | return is64Bits ? ExportDirectoryIndexPE32Plus : ExportDirectoryIndexPE; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/UnityVersion.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Offsets 2 | { 3 | using System; 4 | using System.Linq; 5 | 6 | public struct UnityVersion 7 | { 8 | public static readonly UnityVersion Version2018_4_10 = new UnityVersion(2018, 4, 10); 9 | public static readonly UnityVersion Version2019_4_5 = new UnityVersion(2019, 4, 5); 10 | public static readonly UnityVersion Version2020_3_13 = new UnityVersion(2020, 3, 13); 11 | public static readonly UnityVersion Version2021_3_14 = new UnityVersion(2021, 3, 14); 12 | public static readonly UnityVersion Version2022_3_42 = new UnityVersion(2022, 3, 42); 13 | 14 | public UnityVersion(int year, int versionWithinYear, int subversionWithinYear) 15 | { 16 | this.Year = year; 17 | this.VersionWithinYear = versionWithinYear; 18 | this.SubversionWithinYear = subversionWithinYear; 19 | } 20 | 21 | public int Year { get; } 22 | 23 | public int VersionWithinYear { get; } 24 | 25 | public int SubversionWithinYear { get; } 26 | 27 | public static bool operator ==(UnityVersion a, UnityVersion b) => a.Equals(b); 28 | 29 | public static bool operator !=(UnityVersion a, UnityVersion b) => !(a == b); 30 | 31 | public static UnityVersion Parse(string version) 32 | { 33 | if (version == null) 34 | { 35 | throw new ArgumentNullException("version paramenter cannot be null"); 36 | } 37 | 38 | string[] versionSplit = version.Split('.'); 39 | int subversionWithinYear = int.Parse(new string(versionSplit[2].TakeWhile(char.IsDigit).ToArray())); 40 | return new UnityVersion(int.Parse(versionSplit[0]), int.Parse(versionSplit[1]), subversionWithinYear); 41 | } 42 | 43 | public override bool Equals(object obj) 44 | { 45 | if (obj is UnityVersion other) 46 | { 47 | return other.Year == this.Year && 48 | other.VersionWithinYear == this.VersionWithinYear && 49 | other.SubversionWithinYear == this.SubversionWithinYear; 50 | } 51 | else 52 | { 53 | return false; 54 | } 55 | } 56 | 57 | public override int GetHashCode() 58 | { 59 | int hash = 17; 60 | hash = (hash * 27) + this.Year.GetHashCode(); 61 | hash = (hash * 23) + this.VersionWithinYear.GetHashCode(); 62 | hash = (hash * 13) + this.SubversionWithinYear.GetHashCode(); 63 | return hash; 64 | } 65 | 66 | public override string ToString() 67 | { 68 | return this.Year + "." + this.VersionWithinYear + "." + this.SubversionWithinYear; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ModuleInfo.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | 6 | [DebuggerDisplay("{" + nameof(ModuleInfo.ModuleName) + "}")] 7 | public class ModuleInfo 8 | { 9 | public ModuleInfo(string moduleName, IntPtr baseAddress, uint size, string path) 10 | { 11 | this.ModuleName = moduleName; 12 | this.BaseAddress = baseAddress; 13 | this.Size = size; 14 | this.Path = path; 15 | } 16 | 17 | public IntPtr BaseAddress { get; } 18 | 19 | public string ModuleName { get; } 20 | 21 | public string Path { get; } 22 | 23 | public uint Size { get; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxClient.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// A Linux specific facade over a process that provides access to its memory space 8 | /// through a server running with /proc/$pid/mem read privileges. 9 | /// 10 | [PublicAPI] 11 | public class ProcessFacadeLinuxClient : ProcessFacadeLinux 12 | { 13 | private readonly ProcessFacadeClient client; 14 | 15 | public ProcessFacadeLinuxClient(int processId) 16 | : base(processId) 17 | { 18 | this.client = new ProcessFacadeClient(processId); 19 | } 20 | 21 | protected override void ReadProcessMemory( 22 | byte[] buffer, 23 | IntPtr processAddress, 24 | int length) 25 | => this.client.ReadProcessMemory(buffer, processAddress, length); 26 | } 27 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxDirect.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.IO; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// A Linux specific facade over a process that provides access to its memory space 9 | /// that requires /proc/$pid/mem read privileges. 10 | /// 11 | [PublicAPI] 12 | public class ProcessFacadeLinuxDirect : ProcessFacadeLinux 13 | { 14 | private readonly string memFilePath; 15 | 16 | public ProcessFacadeLinuxDirect(int processId, string memFilePath) 17 | : base(processId) 18 | { 19 | this.memFilePath = memFilePath; 20 | } 21 | 22 | protected unsafe override void ReadProcessMemory( 23 | byte[] buffer, 24 | IntPtr processAddress, 25 | int length) 26 | { 27 | using (FileStream memFileStream = new FileStream(this.memFilePath, FileMode.Open)) 28 | { 29 | memFileStream.Seek(processAddress.ToInt64(), 0); 30 | if (length != memFileStream.Read(buffer, 0, length)) 31 | { 32 | throw new Exception("Error reading data from " + this.memFilePath); 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxDump.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using JetBrains.Annotations; 7 | 8 | /// 9 | /// A Linux specific facade over a process that provides access to its memory space 10 | /// that requires /proc/$pid/mem read privileges. 11 | /// 12 | [PublicAPI] 13 | public class ProcessFacadeLinuxDump : ProcessFacadeLinux 14 | { 15 | private readonly List dumpFiles; 16 | 17 | public ProcessFacadeLinuxDump(string mapsFilePath, string dumpsPath) 18 | : base(mapsFilePath) 19 | { 20 | string[] dumpFilePaths = Directory.GetFiles(dumpsPath); 21 | this.dumpFiles = new List(dumpFilePaths.Length); 22 | string[] splitFileName; 23 | 24 | foreach (var filePath in dumpFilePaths) 25 | { 26 | splitFileName = new FileInfo(filePath).Name.Split('-'); 27 | if (splitFileName.Length == 2) 28 | { 29 | this.dumpFiles.Add(new MemoryMapping(splitFileName[0], splitFileName[1], filePath, false)); 30 | } 31 | } 32 | } 33 | 34 | protected unsafe override void ReadProcessMemory( 35 | byte[] buffer, 36 | IntPtr processAddress, 37 | int length) 38 | { 39 | this.ReadProcessMemory(buffer, processAddress, 0, length); 40 | } 41 | 42 | private unsafe void ReadProcessMemory( 43 | byte[] buffer, 44 | IntPtr processAddress, 45 | int bufferIndex, 46 | int length) 47 | { 48 | if (buffer == null) 49 | { 50 | throw new ArgumentNullException("the buffer parameter cannot be null"); 51 | } 52 | 53 | lock (this.dumpFiles) 54 | { 55 | foreach (MemoryMapping dumpFile in this.dumpFiles) 56 | { 57 | if (dumpFile.Contains(processAddress)) 58 | { 59 | using (FileStream memFileStream = new FileStream(dumpFile.ModuleName, FileMode.Open)) 60 | { 61 | long fileOffset = processAddress.ToInt64() - dumpFile.StartAddress.ToInt64(); 62 | memFileStream.Seek(fileOffset, 0); 63 | 64 | if (fileOffset + length < dumpFile.Size) 65 | { 66 | if (length != memFileStream.Read(buffer, bufferIndex, length)) 67 | { 68 | throw new Exception("Error reading data from " + dumpFile.ModuleName); 69 | } 70 | } 71 | else 72 | { 73 | int bytesToReadInCurrentFile = Convert.ToInt32(dumpFile.Size - fileOffset); 74 | if (bytesToReadInCurrentFile != memFileStream.Read(buffer, bufferIndex, bytesToReadInCurrentFile)) 75 | { 76 | throw new Exception("Error reading data from " + dumpFile.ModuleName); 77 | } 78 | 79 | int newBufferIndex = bufferIndex + bytesToReadInCurrentFile; 80 | int newLength = length - bytesToReadInCurrentFile; 81 | this.ReadProcessMemory(buffer, dumpFile.EndAddress, newBufferIndex, newLength); 82 | } 83 | } 84 | 85 | return; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxPTrace.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// A Linux specific facade over a process that provides access to its memory space through ptrace. 9 | /// 10 | [PublicAPI] 11 | public class ProcessFacadeLinuxPTrace : ProcessFacadeLinux 12 | { 13 | private readonly int processId; 14 | 15 | public ProcessFacadeLinuxPTrace(int processId) 16 | : base(processId) 17 | { 18 | this.processId = processId; 19 | } 20 | 21 | public int ProcessId => this.processId; 22 | 23 | protected unsafe override void ReadProcessMemory( 24 | byte[] buffer, 25 | IntPtr processAddress, 26 | int length) 27 | { 28 | fixed (byte* bytePtr = buffer) 29 | { 30 | var ptr = (IntPtr)bytePtr; 31 | var localIo = new Iovec 32 | { 33 | IovBase = ptr.ToPointer(), 34 | IovLen = length, 35 | }; 36 | var remoteIo = new Iovec 37 | { 38 | IovBase = processAddress.ToPointer(), 39 | IovLen = length, 40 | }; 41 | 42 | var res = ProcessVmReadV(this.ProcessId, &localIo, 1, &remoteIo, 1, 0); 43 | if (res != -1) 44 | { 45 | // Array.Copy(*(byte[]*)ptr, 0, buffer, 0, length); 46 | Marshal.Copy(ptr, buffer, 0, length); 47 | } 48 | else 49 | { 50 | throw new Exception("Error while trying to read memory through from process_vm_readv. Check errno."); 51 | } 52 | } 53 | } 54 | 55 | [DllImport("libc", EntryPoint = "process_vm_readv")] 56 | private static extern unsafe int ProcessVmReadV( 57 | int pid, 58 | Iovec* local_iov, 59 | ulong liovcnt, 60 | Iovec* remote_iov, 61 | ulong riovcnt, 62 | ulong flags); 63 | 64 | [StructLayout(LayoutKind.Sequential)] 65 | private unsafe struct Iovec 66 | { 67 | public void* IovBase; 68 | public int IovLen; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeMacOS.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Runtime.InteropServices; 7 | using JetBrains.Annotations; 8 | 9 | /// 10 | /// A MacOS specific facade over a process that provides access to its memory space. 11 | /// 12 | [PublicAPI] 13 | public abstract class ProcessFacadeMacOS : ProcessFacade 14 | { 15 | private readonly Process process; 16 | 17 | public ProcessFacadeMacOS(Process process) 18 | { 19 | this.process = process; 20 | } 21 | 22 | public Process Process => this.process; 23 | } 24 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeMacOSClient.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// A MacOS specific facade over a process that provides access to its memory space 9 | /// through a server running with root privileges. 10 | /// 11 | [PublicAPI] 12 | public class ProcessFacadeMacOSClient : ProcessFacadeMacOS 13 | { 14 | private readonly ProcessFacadeClient client; 15 | 16 | public ProcessFacadeMacOSClient(Process process) 17 | : base(process) 18 | { 19 | this.client = new ProcessFacadeClient(process.Id); 20 | } 21 | 22 | public override void ReadProcessMemory( 23 | byte[] buffer, 24 | IntPtr processAddress, 25 | bool allowPartialRead = false, 26 | int? size = default) 27 | { 28 | if (buffer == null) 29 | { 30 | throw new ArgumentNullException("the buffer parameter cannot be null"); 31 | } 32 | 33 | this.client.ReadProcessMemory(buffer, processAddress, size ?? buffer.Length); 34 | } 35 | 36 | public override ModuleInfo GetModule(string moduleName) 37 | => this.client.GetModuleInfo(moduleName); 38 | } 39 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeMacOSDirect.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | using JetBrains.Annotations; 7 | 8 | /// 9 | /// A MacOS specific facade over a process that provides access to its memory space. 10 | /// 11 | [PublicAPI] 12 | public class ProcessFacadeMacOSDirect : ProcessFacadeMacOS 13 | { 14 | public ProcessFacadeMacOSDirect(Process process) 15 | : base(process) 16 | { 17 | } 18 | 19 | public override void ReadProcessMemory( 20 | byte[] buffer, 21 | IntPtr processAddress, 22 | bool allowPartialRead = false, 23 | int? size = default) 24 | { 25 | if (buffer == null) 26 | { 27 | throw new ArgumentNullException("the buffer parameter cannot be null"); 28 | } 29 | 30 | var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); 31 | 32 | try 33 | { 34 | var bufferPointer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); 35 | if (ProcessFacadeMacOSDirect.ReadProcessMemory( 36 | this.Process.Id, 37 | processAddress, 38 | bufferPointer, 39 | size ?? buffer.Length) != 0) 40 | { 41 | var error = Marshal.GetLastWin32Error(); 42 | if ((error == 299) && allowPartialRead) 43 | { 44 | return; 45 | } 46 | 47 | throw new InvalidOperationException($"Could not read memory at address {processAddress.ToString("X")}. Error code: {error}"); 48 | } 49 | } 50 | finally 51 | { 52 | bufferHandle.Free(); 53 | } 54 | } 55 | 56 | public override ModuleInfo GetModule(string moduleName) 57 | { 58 | if (!this.Is64Bits) 59 | { 60 | throw new NotSupportedException("MacOS for 32 binaries is not supported currently"); 61 | } 62 | 63 | IntPtr baseAddress; 64 | uint size = 0; 65 | IntPtr buffer = Marshal.AllocCoTaskMem(2048 + 1); 66 | string path; 67 | try 68 | { 69 | Marshal.WriteByte(buffer, 2048, 0); // Ensure there will be a null byte after call 70 | baseAddress = GetModuleInfo(this.Process.Id, moduleName, ref size, buffer); 71 | path = Marshal.PtrToStringAnsi(buffer); 72 | } 73 | finally 74 | { 75 | Marshal.FreeCoTaskMem(buffer); 76 | } 77 | 78 | return new ModuleInfo(moduleName, baseAddress, size, path); 79 | } 80 | 81 | [DllImport("macos.dylib", EntryPoint = "read_process_memory_to_buffer", SetLastError = true)] 82 | private static extern int ReadProcessMemory( 83 | int processId, 84 | IntPtr lpBaseAddress, 85 | IntPtr lpBuffer, 86 | int nSize); 87 | 88 | [DllImport("macos.dylib", EntryPoint = "get_module_info", SetLastError = true)] 89 | private static extern IntPtr GetModuleInfo( 90 | int processId, 91 | string moduleName, 92 | ref uint nSize, 93 | IntPtr path); 94 | } 95 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/UnityProcessFacade.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using HackF5.UnitySpy.Detail; 6 | using HackF5.UnitySpy.Offsets; 7 | using JetBrains.Annotations; 8 | 9 | /// 10 | /// A facade over an unity process that provides access to its memory space. 11 | /// 12 | [PublicAPI] 13 | public class UnityProcessFacade 14 | { 15 | private readonly ProcessFacade process; 16 | 17 | private readonly MonoLibraryOffsets monoLibraryOffsets; 18 | 19 | public UnityProcessFacade(ProcessFacade process, MonoLibraryOffsets monoLibraryOffsets) 20 | { 21 | this.process = process; 22 | this.monoLibraryOffsets = monoLibraryOffsets; 23 | 24 | if (monoLibraryOffsets != null) 25 | { 26 | this.process.Is64Bits = monoLibraryOffsets.Is64Bits; 27 | } 28 | } 29 | 30 | public MonoLibraryOffsets MonoLibraryOffsets => this.monoLibraryOffsets; 31 | 32 | public bool Is64Bits => this.process.Is64Bits; 33 | 34 | public int SizeOfPtr => this.process.SizeOfPtr; 35 | 36 | public ProcessFacade Process => this.process; 37 | 38 | public string ReadAsciiString(IntPtr address, int maxSize = 1024) => 39 | this.process.ReadAsciiString(address, maxSize); 40 | 41 | public string ReadAsciiStringPtr(IntPtr address, int maxSize = 1024) => 42 | this.ReadAsciiString(this.ReadPtr(address), maxSize); 43 | 44 | public int ReadInt32(IntPtr address) => this.process.ReadInt32(address); 45 | 46 | public long ReadInt64(IntPtr address) => this.process.ReadInt64(address); 47 | 48 | public object ReadManaged([NotNull] TypeInfo type, List genericTypeArguments, IntPtr address) 49 | => this.process.ReadManaged(type, genericTypeArguments, address); 50 | 51 | public IntPtr ReadPtr(IntPtr address) => this.process.ReadPtr(address); 52 | 53 | public uint ReadUInt32(IntPtr address) => this.process.ReadUInt32(address); 54 | 55 | public ulong ReadUInt64(IntPtr address) => this.process.ReadUInt64(address); 56 | 57 | public byte ReadByte(IntPtr address) => this.process.ReadByte(address); 58 | 59 | public byte[] ReadModule([NotNull] ModuleInfo moduleInfo) => this.process.ReadModule(moduleInfo); 60 | 61 | public ModuleInfo GetMonoModule() => this.process.GetModule(this.monoLibraryOffsets.MonoLibrary); 62 | } 63 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Util/ByteArrayPool.cs: -------------------------------------------------------------------------------- 1 | // SEE: http://source.roslyn.codeplex.com/#Microsoft.CodeAnalysis.Workspaces/ObjectPool%25601.cs 2 | 3 | namespace HackF5.UnitySpy.Util 4 | { 5 | using System; 6 | using System.Threading; 7 | using JetBrains.Annotations; 8 | 9 | public class ByteArrayPool 10 | { 11 | private const int BufferSize = 0x10; 12 | 13 | private const int MaxBufferCount = 0x10; 14 | 15 | private static readonly byte[] Empty = new byte[ByteArrayPool.BufferSize]; 16 | 17 | private readonly Element[] items; 18 | 19 | private byte[] firstItem; 20 | 21 | private ByteArrayPool() 22 | { 23 | this.items = new Element[ByteArrayPool.MaxBufferCount]; 24 | } 25 | 26 | public static ByteArrayPool Instance { get; } = new ByteArrayPool(); 27 | 28 | public byte[] Rent(int size) 29 | { 30 | if (size > ByteArrayPool.BufferSize) 31 | { 32 | return new byte[size]; 33 | } 34 | 35 | var item = this.firstItem; 36 | if ((item == null) || (item != Interlocked.CompareExchange(ref this.firstItem, null, item))) 37 | { 38 | var itemsCopy = this.items; 39 | for (var i = 0; i < itemsCopy.Length; i++) 40 | { 41 | // Note that the initial read is optimistically not synchronized. That is intentional. 42 | // We will interlock only when we have a candidate. in a worst case we may miss some 43 | // recently returned objects. Not a big deal. 44 | item = itemsCopy[i].Value; 45 | if (item != null) 46 | { 47 | if (item == Interlocked.CompareExchange(ref itemsCopy[i].Value, null, item)) 48 | { 49 | break; 50 | } 51 | } 52 | } 53 | } 54 | 55 | return item ?? new byte[ByteArrayPool.BufferSize]; 56 | } 57 | 58 | public void Return([NotNull] byte[] buffer) 59 | { 60 | if (buffer == null) 61 | { 62 | throw new ArgumentNullException(nameof(buffer)); 63 | } 64 | 65 | if (buffer.Length > ByteArrayPool.BufferSize) 66 | { 67 | return; 68 | } 69 | 70 | // optimistically clear buffer. 71 | Buffer.BlockCopy(ByteArrayPool.Empty, 0, buffer, 0, ByteArrayPool.BufferSize); 72 | 73 | // Intentionally not using interlocked here. 74 | // In a worst case scenario two objects may be stored into same slot. 75 | // It is very unlikely to happen and will only mean that one of the objects will get collected. 76 | if (this.firstItem == null) 77 | { 78 | this.firstItem = buffer; 79 | } 80 | else 81 | { 82 | var itemsCopy = this.items; 83 | for (var i = 0; i < itemsCopy.Length; i++) 84 | { 85 | if (itemsCopy[i].Value == null) 86 | { 87 | itemsCopy[i].Value = buffer; 88 | break; 89 | } 90 | } 91 | } 92 | } 93 | 94 | private struct Element 95 | { 96 | internal byte[] Value; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Util/ConversionUtils.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Util 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | public static class ConversionUtils 8 | { 9 | public static string ToAsciiString(this byte[] buffer, int start = 0) 10 | { 11 | var length = buffer.Skip(start).TakeWhile(b => b != 0).Count(); 12 | return Encoding.ASCII.GetString(buffer, start, length); 13 | } 14 | 15 | public static int ToInt32(this byte[] buffer, int start = 0) => BitConverter.ToInt32(buffer, start); 16 | 17 | public static uint ToUInt32(this byte[] buffer, int start = 0) => BitConverter.ToUInt32(buffer, start); 18 | 19 | public static ulong ToUInt64(this byte[] buffer, int start = 0) => BitConverter.ToUInt64(buffer, start); 20 | 21 | public static char ToChar(this byte[] buffer) => BitConverter.ToChar(buffer, 0); 22 | 23 | public static ushort ToUInt16(this byte[] buffer) => BitConverter.ToUInt16(buffer, 0); 24 | 25 | public static short ToInt16(this byte[] buffer) => BitConverter.ToInt16(buffer, 0); 26 | 27 | public static ulong ToUInt64(this byte[] buffer) => BitConverter.ToUInt64(buffer, 0); 28 | 29 | public static long ToInt64(this byte[] buffer) => BitConverter.ToInt64(buffer, 0); 30 | 31 | public static float ToSingle(this byte[] buffer) => BitConverter.ToSingle(buffer, 0); 32 | 33 | public static double ToDouble(this byte[] buffer) => BitConverter.ToDouble(buffer, 0); 34 | 35 | public static byte ToByte(this byte[] buffer) 36 | { 37 | if (buffer == null || buffer.Length == 0) 38 | { 39 | throw new ArgumentNullException("the buffer parameter cannot be null or empty"); 40 | } 41 | 42 | return buffer[0]; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Util/MemoryReadingUtils.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Util 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | using HackF5.UnitySpy.ProcessFacade; 8 | 9 | public class MemoryReadingUtils 10 | { 11 | private ProcessFacade process; 12 | 13 | private List pointersShown = new List(); 14 | 15 | public MemoryReadingUtils(ProcessFacade process) 16 | { 17 | this.process = process; 18 | } 19 | 20 | public void ReadMemory(IntPtr address, int length, int stepSize = 4, int recursiveDepth = 0) 21 | { 22 | StringBuilder strBuilder = new StringBuilder(); 23 | 24 | this.ReadMemoryRecursive(address, length, stepSize, recursiveDepth, strBuilder); 25 | 26 | File.WriteAllText("Memory Dump - " + DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss.fff") + ".txt", strBuilder.ToString()); 27 | } 28 | 29 | private void ReadMemoryRecursive(IntPtr address, int length, int stepSize, int recursiveDepth, StringBuilder strBuilder) 30 | { 31 | for (int i = 0; i < length; i += stepSize) 32 | { 33 | this.SingleReadMemoryRecursive(IntPtr.Add(address, i), length, stepSize, recursiveDepth, strBuilder); 34 | } 35 | } 36 | 37 | private void SingleReadMemoryRecursive(IntPtr address, int length, int stepSize, int recursiveDepth, StringBuilder strBuilder) 38 | { 39 | string addressStr = address.ToString("X"); 40 | 41 | strBuilder.AppendLine("========================================== Reading Memory at " + addressStr + " Depth = " + recursiveDepth + " ========================================== "); 42 | 43 | var ptr = IntPtr.Zero; 44 | if (address != IntPtr.Zero) 45 | { 46 | try 47 | { 48 | ptr = this.process.ReadPtr(address); 49 | } 50 | catch 51 | { 52 | } 53 | } 54 | 55 | try 56 | { 57 | strBuilder.AppendLine("Value as int32: " + this.process.ReadInt32(address)); 58 | strBuilder.AppendLine("Value as uint32: " + this.process.ReadUInt32(address)); 59 | strBuilder.AppendLine("Value as pointer32: " + this.process.ReadUInt32(address).ToString("X")); 60 | strBuilder.AppendLine("Value as pointer64: " + this.process.ReadUInt64(address).ToString("X")); 61 | 62 | byte[] stringBytes = new byte[stepSize]; 63 | for (int i = 0; i < stepSize; i++) 64 | { 65 | stringBytes[i] = this.process.ReadByte(address + i); 66 | } 67 | 68 | strBuilder.AppendLine("Value as string: " + stringBytes.ToAsciiString()); 69 | strBuilder.AppendLine("Value as string (Unicode): " + Encoding.Unicode.GetString(stringBytes, 0, stepSize)); 70 | } 71 | catch (Exception) 72 | { 73 | strBuilder.AppendLine("No possible values found"); 74 | return; 75 | } 76 | 77 | if (ptr != IntPtr.Zero) 78 | { 79 | if (this.pointersShown.Contains(ptr)) 80 | { 81 | strBuilder.AppendLine("Pointer already shown: " + ptr); 82 | } 83 | else 84 | { 85 | try 86 | { 87 | strBuilder.AppendLine("Value as char *: " + this.process.ReadAsciiString(ptr)); 88 | } 89 | catch 90 | { 91 | } 92 | 93 | if (recursiveDepth > 0) 94 | { 95 | this.ReadMemoryRecursive(ptr, length, stepSize, recursiveDepth - 1, strBuilder); 96 | } 97 | 98 | this.pointersShown.Add(ptr); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/rules.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------