├── Docs ├── Easy.png ├── Hard.png ├── Medium.png ├── Problem.png ├── TrayIcon.png ├── DesertWorld.png ├── FrozenWorld.png ├── GreenWorld.png ├── LevelEditor.png ├── CustomLevel1.png ├── CustomLevel2.png ├── CopyDialogLunarLander.gif └── NuclearWastelandWorld.png ├── Resources └── LunarLander.ico ├── .gitmodules ├── cgmanifest.json ├── Source ├── Program.cs ├── App.config ├── Utils │ ├── WindowCapture.cs │ └── NativeInterop.cs ├── GameOverlay │ ├── GameInterface.cs │ └── OverlayWindow.cs ├── app.manifest ├── LunarLander │ ├── LunarLanderOverlayWindow.cs │ ├── LunarDraw.cs │ └── LunarSim.cs └── CopyWatcher │ ├── CopyWatcherApplication.cs │ └── CopyWatcher.cs ├── .gitignore ├── LICENSE ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs └── Resources.resx ├── packages.config ├── Copy Dialog Lunar Lander.sln ├── .gitattributes ├── README.md └── Copy Dialog Lunar Lander.csproj /Docs/Easy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/Easy.png -------------------------------------------------------------------------------- /Docs/Hard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/Hard.png -------------------------------------------------------------------------------- /Docs/Medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/Medium.png -------------------------------------------------------------------------------- /Docs/Problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/Problem.png -------------------------------------------------------------------------------- /Docs/TrayIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/TrayIcon.png -------------------------------------------------------------------------------- /Docs/DesertWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/DesertWorld.png -------------------------------------------------------------------------------- /Docs/FrozenWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/FrozenWorld.png -------------------------------------------------------------------------------- /Docs/GreenWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/GreenWorld.png -------------------------------------------------------------------------------- /Docs/LevelEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/LevelEditor.png -------------------------------------------------------------------------------- /Docs/CustomLevel1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/CustomLevel1.png -------------------------------------------------------------------------------- /Docs/CustomLevel2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/CustomLevel2.png -------------------------------------------------------------------------------- /Resources/LunarLander.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Resources/LunarLander.ico -------------------------------------------------------------------------------- /Docs/CopyDialogLunarLander.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/CopyDialogLunarLander.gif -------------------------------------------------------------------------------- /Docs/NuclearWastelandWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanakan8472/copy-dialog-lunar-lander/HEAD/Docs/NuclearWastelandWorld.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "box2d-netstandard"] 2 | path = box2d-netstandard 3 | url = https://github.com/Sanakan8472/box2d-netstandard 4 | -------------------------------------------------------------------------------- /cgmanifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Registrations": [ 3 | { 4 | "component": { 5 | "type": "git", 6 | "git": { 7 | "repositoryUrl": "https://github.com/codingben/box2d-netstandard", 8 | "commitHash": "113db001379a1d485125fd932a9918304cfadb75" 9 | } 10 | } 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /Source/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | 4 | namespace CopyDialogLunarLander 5 | { 6 | class Program 7 | { 8 | [STAThread] 9 | static void Main(string[] args) 10 | { 11 | CopyWatcherApplication app = new CopyWatcherApplication(); 12 | app.Run(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | bin/ 34 | packages/ 35 | obj/ 36 | .vs/ 37 | -------------------------------------------------------------------------------- /Source/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sanakan8472 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 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CopyDialogLunarLander")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CopyDialogLunarLander")] 13 | [assembly: AssemblyCopyright("Copyright © 2022")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("c244f3b7-8b79-48fd-ad76-ca5583811d8d")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.2.0.0")] 36 | [assembly: AssemblyFileVersion("1.2.0.0")] 37 | -------------------------------------------------------------------------------- /Source/Utils/WindowCapture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | namespace CopyDialogLunarLander 5 | { 6 | class WindowCapture 7 | { 8 | /// 9 | /// Captures the given native window as a bitmap. 10 | /// 11 | /// 12 | /// 13 | public static Bitmap Capture(IntPtr sourceWindow) 14 | { 15 | Bitmap bitmap = null; 16 | try 17 | { 18 | NativeInterop.RECT area; 19 | if (!NativeInterop.GetWindowRect(sourceWindow, out area)) 20 | { 21 | return bitmap; 22 | } 23 | 24 | IntPtr screenDC = NativeInterop.GetDCEx((sourceWindow), IntPtr.Zero, NativeInterop.DeviceContextValues.Validate); 25 | IntPtr memDC = NativeInterop.CreateCompatibleDC(screenDC); 26 | IntPtr hBitmap = NativeInterop.CreateCompatibleBitmap(screenDC, (int)area.Width, (int)area.Height); 27 | NativeInterop.SelectObject(memDC, hBitmap); // Select bitmap from compatible bitmap to memDC 28 | 29 | NativeInterop.BitBlt(memDC, 0, 0, (int)area.Width, (int)area.Height, screenDC, 0, 0, NativeInterop.TernaryRasterOperations.SRCCOPY); 30 | bitmap = Image.FromHbitmap(hBitmap); 31 | 32 | NativeInterop.DeleteObject(hBitmap); 33 | NativeInterop.DeleteDC(memDC); 34 | NativeInterop.ReleaseDC(IntPtr.Zero, screenDC); 35 | } 36 | catch (Exception) 37 | { 38 | 39 | } 40 | return bitmap; 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Copy Dialog Lunar Lander.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34018.315 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Copy Dialog Lunar Lander", "Copy Dialog Lunar Lander.csproj", "{C244F3B7-8B79-48FD-AD76-CA5583811D8D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Box2D.NetStandard", "box2d-netstandard\src\box2dx\Box2D.NetStandard\Box2D.NetStandard.csproj", "{8A1FCE19-725D-4DF7-83A4-079752BE2E9C}" 9 | EndProject 10 | Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Installer", "Installer\Installer.vdproj", "{AB55BF39-46F3-4576-B9E9-76EF39256BF3}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {C244F3B7-8B79-48FD-AD76-CA5583811D8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {C244F3B7-8B79-48FD-AD76-CA5583811D8D}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {C244F3B7-8B79-48FD-AD76-CA5583811D8D}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {C244F3B7-8B79-48FD-AD76-CA5583811D8D}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {8A1FCE19-725D-4DF7-83A4-079752BE2E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {8A1FCE19-725D-4DF7-83A4-079752BE2E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {8A1FCE19-725D-4DF7-83A4-079752BE2E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {8A1FCE19-725D-4DF7-83A4-079752BE2E9C}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {AB55BF39-46F3-4576-B9E9-76EF39256BF3}.Debug|Any CPU.ActiveCfg = Debug 27 | {AB55BF39-46F3-4576-B9E9-76EF39256BF3}.Release|Any CPU.ActiveCfg = Release 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(ExtensibilityGlobals) = postSolution 33 | SolutionGuid = {CB63D6F4-F9E2-477D-93FD-4AD2EE3A0E26} 34 | EndGlobalSection 35 | EndGlobal 36 | -------------------------------------------------------------------------------- /Source/GameOverlay/GameInterface.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Drawing.Imaging; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Automation; 10 | using System.Windows.Controls; 11 | using System.Windows.Media; 12 | using System.Windows.Threading; 13 | using System.Windows.Interop; 14 | using System.Runtime.InteropServices; 15 | using System.Windows.Input; 16 | using System.Windows.Media.Imaging; 17 | using System.Diagnostics; 18 | using System.ComponentModel; 19 | 20 | 21 | namespace CopyDialogLunarLander 22 | { 23 | /// 24 | /// Any new game needs to derive from OverlayWindow and implement this GameInterface. 25 | /// 26 | interface GameInterface 27 | { 28 | /// 29 | /// Called once to define the virtual game space. This is usually 395x85. 30 | /// 31 | /// Logical size of the world. This is independent of DPI scaling and does not neccessarily match pixels. 32 | void Init(System.Windows.Size worldSize); 33 | 34 | /// 35 | /// Called before the window is destroyed. Use this for any game related cleanup. 36 | /// 37 | void DeInit(); 38 | 39 | /// 40 | /// Each frame a height field is extracted from the progress chart. 41 | /// 42 | /// Element count is worldSize.width and values are between 0 (bottom) and 1 (top). Due to DPI scaling the values can be between units of the virtual game space. 43 | /// The color of the copy progress bar 44 | void HeightFieldUpdated(float[] heightField, System.Drawing.Color terrainColor); 45 | 46 | /// 47 | /// Called at 60hz. This is the main game loop function. 48 | /// 49 | /// Render any content to this image 50 | /// Use this to determine time stepping of the simulation and profiling 51 | void Update(DrawingGroup backingStore, OverlayStats stats); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /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 CopyDialogLunarLander.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("CopyDialogLunarLander.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 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 65 | /// 66 | internal static System.Drawing.Icon LunarLander { 67 | get { 68 | object obj = ResourceManager.GetObject("LunarLander", resourceCulture); 69 | return ((System.Drawing.Icon)(obj)); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Source/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | true 58 | PerMonitor 59 | 60 | 61 | 62 | 63 | 64 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Copy Dialog Lunar Lander 2 | 3 | Here is one of the most pressing issues of our times: You need to download a huge file from the other side of the world to continue your work. You literally can't do anything but wait. 4 | 5 | ![Problem](Docs/Problem.png) 6 | 7 | Now there is finally a solution to this problem! Play lunar lander in these inviting hills generated by the progress dialog. Time flies when you are having fun! 8 | 9 | ![CopyDialogLunarLander](Docs/CopyDialogLunarLander.gif) 10 | 11 | ## Multiple difficulty settings! 12 | There are multiple difficulty settings to choose from: 13 | * **Easy**: Just copy a huge file from a local SSD to another local SSD.\ 14 | ![Easy](Docs/Easy.png) 15 | * **Medium**: Copy a big file to a crappy USB 2.0 stick.\ 16 | ![Medium](Docs/Medium.png) 17 | * **Hard**: Probably your work VPN that connects you to the office on the other side of the world. As a plus point, you have more time to complete the level here. Way more time.\ 18 | ![Hard](Docs/Hard.png) 19 | 20 | ## Infinite levels, infinite replayability! 21 | There are infinite levels, thanks to general unreliability of storage devices, the internet and VPNs. 22 | Even playing the same 'level' twice might result in totally different landscapes, infinite replayability is guaranteed! 23 | 24 | ## Visit many exciting worlds! 25 | Unlike the lunar lander from the olden times, this one is not limited to visiting just the moon, here are some other worlds you can visit: 26 | * **Lush green forest**: The default world you visit. Just copy any file to reach this world.\ 27 | ![GreenWorld](Docs/GreenWorld.png) 28 | * **Scorching hot desert**: Just press *pause* on your copy operation to visit this world.\ 29 | ![DesertWorld](Docs/DesertWorld.png) 30 | * **Frozen ice planet**: You can visit this world by enabling *high contrast mode* in Windows.\ 31 | ![FrozenWorld](Docs/FrozenWorld.png) 32 | * **Nuclear wasteland**: By combining *pause* and *high contrast mode* from above you can unlock this nightmarish world.\ 33 | ![NuclearWastelandWorld](Docs/NuclearWastelandWorld.png) 34 | 35 | ## Create your own worlds (with the OS built-in level editor)! 36 | If the above worlds are not your cup of tea, you can create your own! Simply enable *high contrast mode* and change the *Hyperlinks* color for the *paused* world and the *Selected Text* color for the normal world. That's 16.7 million worlds that you can visit! 37 | 38 | ![LevelEditor](Docs/LevelEditor.png)\ 39 | Here some examples of what worlds await you: 40 | 41 | ![CustomLevel1](Docs/CustomLevel1.png) 42 | ![CustomLevel2](Docs/CustomLevel2.png) 43 | 44 | # Building 45 | 46 | Needs at least Windows 10 for handling dpi scaling correctly. Only tested on Windows 10 21H2 so far. Windows 11 is untested. 47 | 48 | * `git clone` into some folder and go there. 49 | * `git submodule init` 50 | * `git submodule update` 51 | * Open `Copy Dialog Lunar Lander.sln` in Visual Studio 2019. 52 | * Build and run. 53 | 54 | # Gameplay 55 | 56 | Once running, a tray icon is added that can be used to exit the app, change the difficulty in case your moon lander explodes even on flat terrain. 57 | * **Hard** means you need to be below 5m/s when touching the ground. 58 | * **Easy** eases the limit to below 10m/s. 59 | 60 | ![TrayIcon](Docs/TrayIcon.png) 61 | 62 | While running, the app will detect all windows copy dialogs and paint a game overlay on top of them while they are focused. You can then click in the progress graph to begin playing. 63 | Controls: 64 | * **left, right, down** arrow keys control the thrusters. 65 | * **space** restarts the game. 66 | * **tab** enables debug rendering and frame statistics which can be used to see how badly optimized my code is. 67 | 68 | # Contributing 69 | 70 | Contributions are always welcome!\ 71 | You can even make new games by deriving from `OverlayWindow` and implementing `GameInterface`. Any non-abstract class will show up in the tray icon so you can switch to your new game.\ 72 | Your imagination is only limited by the 395x85 logical units size of the progress chart. 73 | 74 | If you have too much time on your hands, consider contributing to [box2d-standard](https://github.com/codingben/box2d-netstandard) instead. It's used in this project to drive the lunar lander (mostly into the ground and then to explode). 75 | 76 | # Known Issues 77 | 78 | * Sometimes clicking on the **Click here to play** message does not work and the game overlay gets unfocused. Not sure why that is. Just click twice again until it works. 79 | * Dragging from one monitor to another with a different DPI causes the overlay to spasm out. Just drag the copy dialog a bit further and it will fix itself. Not sure why that happens but I already wasted too much time on this joke so I left it as is but hey, [contributions](#contributing) are always welcome! 80 | -------------------------------------------------------------------------------- /Source/Utils/NativeInterop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace CopyDialogLunarLander 5 | { 6 | class NativeInterop 7 | { 8 | #region user32 Imports 9 | 10 | [DllImport("user32.dll")] 11 | public static extern uint GetDpiForWindow(IntPtr hwnd); 12 | 13 | public const Int32 MONITOR_DEFAULTTONEAREST = 0x00000002; 14 | 15 | [Flags()] 16 | public enum DeviceContextValues : uint 17 | { 18 | Window = 0x00000001, 19 | Cache = 0x00000002, 20 | NoResetAttrs = 0x00000004, 21 | ClipChildren = 0x00000008, 22 | ClipSiblings = 0x00000010, 23 | ParentClip = 0x00000020, 24 | ExcludeRgn = 0x00000040, 25 | IntersectRgn = 0x00000080, 26 | ExcludeUpdate = 0x00000100, 27 | IntersectUpdate = 0x00000200, 28 | LockWindowUpdate = 0x00000400, 29 | UseStyle = 0x00010000, 30 | Validate = 0x00200000, 31 | } 32 | 33 | [DllImport("user32.dll")] 34 | public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, DeviceContextValues flags); 35 | 36 | [DllImport("user32.dll")] 37 | public static extern IntPtr GetDC(IntPtr hWnd); 38 | 39 | [StructLayout(LayoutKind.Sequential)] 40 | public struct POINT 41 | { 42 | public int x; 43 | public int y; 44 | } 45 | 46 | [StructLayout(LayoutKind.Sequential)] 47 | public struct RECT 48 | { 49 | public int Left; // x position of upper-left corner 50 | public int Top; // y position of upper-left corner 51 | public int Right; // x position of lower-right corner 52 | public int Bottom; // y position of lower-right corner 53 | 54 | public double Width 55 | { 56 | get { return Right - Left; } 57 | } 58 | 59 | public double Height 60 | { 61 | get { return Bottom - Top; } 62 | } 63 | } 64 | 65 | [DllImport("user32.dll")] 66 | public static extern IntPtr GetParent(IntPtr hWnd); 67 | 68 | [DllImport("user32.dll")] 69 | [return: MarshalAs(UnmanagedType.Bool)] 70 | public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); 71 | 72 | [DllImport("user32.dll")] 73 | public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); 74 | 75 | [DllImport("user32.dll")] 76 | public static extern bool IsWindowVisible(IntPtr hWnd); 77 | 78 | [DllImport("user32.dll")] 79 | public static extern IntPtr GetForegroundWindow(); 80 | 81 | [DllImport("user32.dll")] 82 | public static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd, out uint ProcessId); 83 | 84 | #endregion user32 Imports 85 | 86 | #region gdi32 Imports 87 | 88 | [DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)] 89 | public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); 90 | 91 | [DllImport("gdi32.dll")] 92 | public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight); 93 | 94 | [DllImport("gdi32.dll", SetLastError = true)] 95 | public static extern IntPtr CreateCompatibleDC(IntPtr hdc); 96 | 97 | [DllImport("gdi32.dll")] 98 | public static extern bool DeleteObject(IntPtr hObject); 99 | 100 | [DllImport("gdi32.dll")] 101 | public static extern bool DeleteDC(IntPtr hObject); 102 | 103 | [DllImport("gdi32.dll")] 104 | public static extern IntPtr CreateBitmap(int nWidth, int nHeight, uint cPlanes, uint cBitsPerPel, IntPtr lpvBits); 105 | 106 | public enum TernaryRasterOperations : uint 107 | { 108 | SRCCOPY = 0x00CC0020, 109 | SRCPAINT = 0x00EE0086, 110 | SRCAND = 0x008800C6, 111 | SRCINVERT = 0x00660046, 112 | SRCERASE = 0x00440328, 113 | NOTSRCCOPY = 0x00330008, 114 | NOTSRCERASE = 0x001100A6, 115 | MERGECOPY = 0x00C000CA, 116 | MERGEPAINT = 0x00BB0226, 117 | PATCOPY = 0x00F00021, 118 | PATPAINT = 0x00FB0A09, 119 | PATINVERT = 0x005A0049, 120 | DSTINVERT = 0x00550009, 121 | BLACKNESS = 0x00000042, 122 | WHITENESS = 0x00FF0062, 123 | } 124 | 125 | [DllImport("gdi32.dll")] 126 | public static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, TernaryRasterOperations dwRop); 127 | 128 | #endregion gdi32 Imports 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Source/LunarLander/LunarLanderOverlayWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Drawing.Imaging; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Input; 10 | using System.Windows.Media; 11 | 12 | namespace CopyDialogLunarLander 13 | { 14 | public class LunarLanderOverlayWindow : OverlayWindow 15 | { 16 | private LunarSim _sim = new LunarSim(); 17 | private bool _left = false; 18 | private bool _right = false; 19 | private bool _down = false; 20 | private bool _debug = false; 21 | 22 | private static System.Windows.Forms.ToolStripMenuItem _menuEasy; 23 | private static System.Windows.Forms.ToolStripMenuItem _menuHard; 24 | 25 | public LunarLanderOverlayWindow() 26 | { 27 | } 28 | 29 | #region FillOptions 30 | static void FillOptions(System.Windows.Forms.ToolStripMenuItem optionsMenu) 31 | { 32 | if (_menuEasy == null) 33 | { 34 | _menuEasy = (System.Windows.Forms.ToolStripMenuItem)optionsMenu.DropDownItems.Add("Easy"); 35 | _menuEasy.Click += EasySelected; 36 | } 37 | else 38 | { 39 | optionsMenu.DropDownItems.Add(_menuEasy); 40 | } 41 | if (_menuHard == null) 42 | { 43 | _menuHard = (System.Windows.Forms.ToolStripMenuItem)optionsMenu.DropDownItems.Add("Hard"); 44 | _menuHard.Click += HardSelected; 45 | _menuHard.Checked = true; 46 | } 47 | else 48 | { 49 | optionsMenu.DropDownItems.Add(_menuHard); 50 | } 51 | } 52 | 53 | 54 | private static void EasySelected(object sender, EventArgs e) 55 | { 56 | LunarSim.k_maxSpeed = 10; 57 | _menuEasy.Checked = true; 58 | _menuHard.Checked = false; 59 | } 60 | private static void HardSelected(object sender, EventArgs e) 61 | { 62 | LunarSim.k_maxSpeed = 5; 63 | _menuEasy.Checked = false; 64 | _menuHard.Checked = true; 65 | } 66 | 67 | #endregion FillOptions 68 | 69 | #region GameInterface 70 | 71 | public override void Init(System.Windows.Size worldSize) 72 | { 73 | _sim.Init(worldSize); 74 | } 75 | 76 | public override void DeInit() 77 | { 78 | _sim.Dispose(); 79 | } 80 | 81 | public override void HeightFieldUpdated(float[] heightField, System.Drawing.Color terrainColor) 82 | { 83 | _sim.SetHeightField(heightField, terrainColor); 84 | } 85 | 86 | public override void Update(DrawingGroup _backingStore, OverlayStats stats) 87 | { 88 | _sim.SetActive(this.IsActive); 89 | _sim.Input(_left, _right, _down); 90 | 91 | var drawingContext = _backingStore.Open(); 92 | 93 | // The drawingContext coordinate system has 0,0 in the upper left corner. 94 | // However, for box2d it's the lower left corner so we flip the coordinate space here. 95 | drawingContext.PushTransform(new ScaleTransform(1 / 1, -1 /1)); 96 | drawingContext.PushTransform(new TranslateTransform(0, -Height * 1)); 97 | // This will execute the debug render hook of box2d which we use to render our scene. 98 | _sim.Step(1.0f / (float)stats.fps, drawingContext); 99 | drawingContext.Pop(); 100 | drawingContext.Pop(); 101 | // We don't want text to be upside-down so the normal render method is outside the transform block above. 102 | _sim.Render(drawingContext); 103 | 104 | if (_debug) 105 | { 106 | LunarSceneDraw.DrawText(drawingContext, stats.GetStatsString(), new System.Windows.Point(4, Height - 12), 12D, System.Drawing.Color.Black); 107 | } 108 | drawingContext.Close(); 109 | } 110 | 111 | #endregion GameInterface 112 | 113 | #region Input 114 | 115 | protected override void OnKeyDown(KeyEventArgs e) 116 | { 117 | if (e.Key == Key.Down) 118 | _down = true; 119 | if (e.Key == Key.Left) 120 | _left = true; 121 | if (e.Key == Key.Right) 122 | _right = true; 123 | if (e.Key == Key.Space) 124 | _sim.Reset(); 125 | base.OnKeyDown(e); 126 | } 127 | 128 | protected override void OnKeyUp(KeyEventArgs e) 129 | { 130 | if (e.Key == Key.Down) 131 | _down = false; 132 | if (e.Key == Key.Left) 133 | _left = false; 134 | if (e.Key == Key.Right) 135 | _right = false; 136 | if (e.Key == Key.Tab) 137 | { 138 | _debug = !_debug; 139 | _sim.SetDebug(_debug); 140 | } 141 | base.OnKeyUp(e); 142 | } 143 | 144 | protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) 145 | { 146 | // Debug code, adds a box under the cursor 147 | /* 148 | PresentationSource MainWindowPresentationSource = PresentationSource.FromVisual(this); 149 | Matrix m = MainWindowPresentationSource.CompositionTarget.TransformToDevice; 150 | 151 | var scenePos = e.GetPosition(this); 152 | scenePos.Y = Height - scenePos.Y; 153 | _sim.AddBox(scenePos); 154 | */ 155 | } 156 | 157 | #endregion Input 158 | 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Resources\LunarLander.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | -------------------------------------------------------------------------------- /Source/LunarLander/LunarDraw.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using Box2DX.Common; 8 | using Box2DX.Collision; 9 | using Box2DX.Dynamics; 10 | using System.Windows.Media; 11 | using System.Windows; 12 | 13 | namespace CopyDialogLunarLander 14 | { 15 | /// 16 | /// Use when rendering the scene. Only renders solid polys. 17 | /// 18 | public class LunarSceneDraw : LunarDraw 19 | { 20 | public Box2DX.Dynamics.Color OverrideColor; 21 | 22 | public override void DrawPolygon(Vec2[] vertices, int vertexCount, Box2DX.Dynamics.Color color) 23 | { 24 | } 25 | 26 | public override void DrawSolidPolygon(Vec2[] vertices, int vertexCount, Box2DX.Dynamics.Color color) 27 | { 28 | base.DrawSolidPolygon(vertices, vertexCount, OverrideColor); 29 | } 30 | 31 | public override void DrawCircle(Vec2 center, float radius, Box2DX.Dynamics.Color color) 32 | { 33 | } 34 | 35 | public override void DrawSolidCircle(Vec2 center, float radius, Vec2 axis, Box2DX.Dynamics.Color color) 36 | { 37 | DrawingContext.DrawEllipse(ToBrush(OverrideColor), ToPen(OverrideColor), ToPoint(center), radius, radius); 38 | } 39 | 40 | public override void DrawSegment(Vec2 p1, Vec2 p2, Box2DX.Dynamics.Color color) 41 | { 42 | } 43 | 44 | public override void DrawXForm(XForm xf) 45 | { 46 | } 47 | } 48 | 49 | /// 50 | /// For debug visualization. Draws everything as red wireframe. 51 | /// 52 | public class LunarDebugSceneDraw : LunarDraw 53 | { 54 | private Box2DX.Dynamics.Color _debugColor = new Box2DX.Dynamics.Color(1, 0, 0); 55 | 56 | public override void DrawSolidPolygon(Box2DX.Common.Vec2[] vertices, int vertexCount, Box2DX.Dynamics.Color color) 57 | { 58 | DrawPolygon(vertices, vertexCount, _debugColor); 59 | } 60 | 61 | public override void DrawCircle(Box2DX.Common.Vec2 center, float radius, Box2DX.Dynamics.Color color) 62 | { 63 | DrawingContext.DrawEllipse(null, ToPen(_debugColor), ToPoint(center), radius, radius); 64 | } 65 | 66 | public override void DrawSolidCircle(Box2DX.Common.Vec2 center, float radius, Box2DX.Common.Vec2 axis, Box2DX.Dynamics.Color color) 67 | { 68 | DrawCircle(center, radius, _debugColor); 69 | } 70 | 71 | public override void DrawSegment(Box2DX.Common.Vec2 p1, Box2DX.Common.Vec2 p2, Box2DX.Dynamics.Color color) 72 | { 73 | DrawingContext.DrawLine(ToPen(_debugColor), ToPoint(p1), ToPoint(p2)); 74 | } 75 | } 76 | 77 | /// 78 | /// Draws on the DrawingContext of the file copy dialog. 79 | /// 80 | public class LunarDraw : DebugDraw 81 | { 82 | public System.Windows.Media.DrawingContext DrawingContext { get; set; } 83 | 84 | public override void DrawPolygon(Box2DX.Common.Vec2[] vertices, int vertexCount, Box2DX.Dynamics.Color color) 85 | { 86 | for (int i = 0; i < vertexCount; i++) 87 | { 88 | DrawSegment(vertices[i], vertices[(i + 1) % vertexCount], color); 89 | } 90 | } 91 | 92 | public override void DrawSolidPolygon(Box2DX.Common.Vec2[] vertices, int vertexCount, Box2DX.Dynamics.Color color) 93 | { 94 | StreamGeometry geo = new StreamGeometry(); 95 | geo.FillRule = FillRule.EvenOdd; 96 | 97 | // Open the context to use for drawing. 98 | using (StreamGeometryContext context = geo.Open()) 99 | { 100 | var vertices2 = vertices.Take(vertexCount); 101 | context.BeginFigure(ToPoint(vertices2.First()), true, true); 102 | context.PolyLineTo(vertices2.Skip(1).Select(x => ToPoint(x)).ToArray(), true, false); 103 | } 104 | DrawingContext.DrawGeometry(ToBrush(color), ToPen(color), geo); 105 | } 106 | 107 | public override void DrawCircle(Box2DX.Common.Vec2 center, float radius, Box2DX.Dynamics.Color color) 108 | { 109 | DrawingContext.DrawEllipse(null, ToPen(color), ToPoint(center), radius, radius); 110 | } 111 | 112 | public override void DrawSolidCircle(Box2DX.Common.Vec2 center, float radius, Box2DX.Common.Vec2 axis, Box2DX.Dynamics.Color color) 113 | { 114 | DrawCircle(center, radius, color); 115 | } 116 | 117 | public override void DrawSegment(Box2DX.Common.Vec2 p1, Box2DX.Common.Vec2 p2, Box2DX.Dynamics.Color color) 118 | { 119 | DrawingContext.DrawLine(ToPen(color), ToPoint(p1), ToPoint(p2)); 120 | } 121 | 122 | public override void DrawXForm(Box2DX.Common.XForm xf) 123 | { 124 | Vec2 p1 = xf.Position, p2; 125 | float k_axisScale = 0.4f; 126 | p2 = p1 + k_axisScale * xf.R.Col1; 127 | DrawSegment(p1, p2, new Box2DX.Dynamics.Color(1.0f, 0.0f, 0.0f)); 128 | p2 = p1 + k_axisScale * xf.R.Col2; 129 | DrawSegment(p1, p2, new Box2DX.Dynamics.Color(0.0f, 1.0f, 0.0f)); 130 | } 131 | 132 | public static void DrawText(DrawingContext drawingContext, string text, System.Windows.Point pos, double size, System.Drawing.Color color) 133 | { 134 | FormattedText ft = new FormattedText(text, 135 | new System.Globalization.CultureInfo("en-us"), 136 | FlowDirection.LeftToRight, 137 | new Typeface(new System.Windows.Media.FontFamily("Consolas"), FontStyles.Normal, FontWeights.Normal, new FontStretch()), 138 | size, 139 | new SolidColorBrush(new System.Windows.Media.Color() { R = color.R, G = color.G, B = color.B, A = 255 })); 140 | drawingContext.DrawText(ft, pos); 141 | } 142 | public System.Windows.Point ToPoint(Box2DX.Common.Vec2 point) 143 | { 144 | return new System.Windows.Point(point.X, point.Y); 145 | } 146 | 147 | public System.Windows.Media.Pen ToPen(Box2DX.Dynamics.Color color) 148 | { 149 | System.Windows.Media.Pen pen = new System.Windows.Media.Pen(); 150 | pen.Brush = new SolidColorBrush(new System.Windows.Media.Color() { R = (byte)(color.R * 255.0f), G = (byte)(color.G * 255.0f), B = (byte)(color.B * 255.0f), A = 255 }); 151 | return pen; 152 | } 153 | 154 | public System.Windows.Media.Brush ToBrush(Box2DX.Dynamics.Color color) 155 | { 156 | var Brush = new SolidColorBrush(new System.Windows.Media.Color() { R = (byte)(color.R * 255.0f), G = (byte)(color.G * 255.0f), B = (byte)(color.B * 255.0f), A = 255 }); 157 | return Brush; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Source/CopyWatcher/CopyWatcherApplication.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Threading; 8 | using System.Windows.Automation; 9 | using System.ComponentModel; 10 | using System.Reflection; 11 | using Microsoft.Win32; 12 | 13 | namespace CopyDialogLunarLander 14 | { 15 | /// 16 | /// Dummy window that is never shown but needs to exist to prevent the app from exiting. 17 | /// 18 | class MainWindow : Window 19 | { 20 | } 21 | 22 | /// 23 | /// CopyWatcherApplication application creates a new OverlayWindow for every copy dialog and manages the tray icon. 24 | /// 25 | class CopyWatcherApplication : Application 26 | { 27 | struct OverlayWindowData 28 | { 29 | public OverlayWindow window; 30 | public AutomationElement chartView; 31 | public IntPtr parentOperationStatusWindow; 32 | } 33 | 34 | struct Game 35 | { 36 | public Type type; 37 | public System.Windows.Forms.ToolStripMenuItem menuItem; 38 | } 39 | 40 | private System.Windows.Forms.NotifyIcon _notifyIcon; 41 | 42 | private OperationStatusWindowWatcher _watcher = new OperationStatusWindowWatcher(); 43 | private Dictionary _overlayWindows = new Dictionary(); 44 | private List _games = new List(); 45 | private Type _currentGame = null; 46 | private System.Windows.Forms.ToolStripMenuItem _gameOptions = null; 47 | private System.Windows.Forms.ToolStripMenuItem _autostart = null; 48 | 49 | const string _autostartRegKey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; 50 | const string _autostartName = "CopyDialogLunarLander"; 51 | 52 | public CopyWatcherApplication() 53 | { 54 | } 55 | 56 | protected override void OnStartup(StartupEventArgs e) 57 | { 58 | base.OnStartup(e); 59 | // Dummy window that is never shown just so the app doesn't exit if no file ops are active. 60 | MainWindow = new MainWindow(); 61 | 62 | _notifyIcon = new System.Windows.Forms.NotifyIcon(); 63 | _notifyIcon.Icon = CopyDialogLunarLander.Properties.Resources.LunarLander; 64 | _notifyIcon.Visible = true; 65 | CreateContextMenu(); 66 | 67 | _watcher.ChartViewOpened += OnChartViewOpened; 68 | _watcher.ChartViewClosed += OnChartViewClosed; 69 | _watcher.Start(); 70 | } 71 | 72 | private void CreateContextMenu() 73 | { 74 | _notifyIcon.ContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(); 75 | { 76 | var gamesMenu = (System.Windows.Forms.ToolStripMenuItem)_notifyIcon.ContextMenuStrip.Items.Add("Games"); 77 | var type = typeof(OverlayWindow); 78 | var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes()).Where(p => type.IsAssignableFrom(p) && p != type && !p.IsAbstract).ToList(); 79 | _currentGame = typeof(LunarLanderOverlayWindow); 80 | foreach (var derivedType in types) 81 | { 82 | var game = (System.Windows.Forms.ToolStripMenuItem)gamesMenu.DropDownItems.Add("Lunar Lander"); 83 | game.Checked = derivedType == _currentGame; 84 | game.Click += GameChanged; 85 | _games.Add(new Game { type = derivedType, menuItem = game }); 86 | } 87 | } 88 | _gameOptions = (System.Windows.Forms.ToolStripMenuItem)_notifyIcon.ContextMenuStrip.Items.Add("Game Options"); 89 | _autostart = (System.Windows.Forms.ToolStripMenuItem)_notifyIcon.ContextMenuStrip.Items.Add("Start with Windows"); 90 | _autostart.Click += (s, e) => ToggleAutostart(); 91 | _autostart.Checked = IsAutostartEnabled(); 92 | _notifyIcon.ContextMenuStrip.Items.Add("-"); 93 | _notifyIcon.ContextMenuStrip.Items.Add("Exit Lunar Lander").Click += (s, e) => ExitApplication(); 94 | 95 | UpdateGameOptions(); 96 | } 97 | 98 | private void GameChanged(object sender, EventArgs e) 99 | { 100 | try 101 | { 102 | var game = _games.FirstOrDefault(x => x.menuItem == sender); 103 | if (game.type != null)// && !game.menuItem.Checked) 104 | { 105 | _currentGame = game.type; 106 | _games.ForEach(x => x.menuItem.Checked = x.menuItem == game.menuItem); 107 | 108 | var values = _overlayWindows.Values.ToList(); 109 | foreach (var item in values) 110 | { 111 | DestroyOverlay(item.chartView); 112 | } 113 | foreach (var item in values) 114 | { 115 | CreateOverlay(item.chartView, item.parentOperationStatusWindow); 116 | } 117 | UpdateGameOptions(); 118 | } 119 | } 120 | catch (Exception ex) 121 | { 122 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 123 | } 124 | } 125 | 126 | private void UpdateGameOptions() 127 | { 128 | _gameOptions.DropDownItems.Clear(); 129 | 130 | MethodInfo info = _currentGame.GetMethod("FillOptions", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); 131 | 132 | object value = info.Invoke(null, new object[] { _gameOptions }); 133 | } 134 | 135 | private bool IsAutostartEnabled() 136 | { 137 | try 138 | { 139 | RegistryKey registryKey = Registry.CurrentUser.OpenSubKey(_autostartRegKey, false); 140 | var value = registryKey.GetValue(_autostartName); 141 | string exePath = Assembly.GetEntryAssembly().Location; 142 | if (value is string && (string)value == exePath) 143 | { 144 | return true; 145 | } 146 | } 147 | catch (Exception ex) 148 | { 149 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 150 | } 151 | return false; 152 | } 153 | 154 | private void ToggleAutostart() 155 | { 156 | try 157 | { 158 | bool enabled = IsAutostartEnabled(); 159 | RegistryKey registryKey = Registry.CurrentUser.OpenSubKey(_autostartRegKey, true); 160 | if (!enabled) 161 | { 162 | string exePath = Assembly.GetEntryAssembly().Location; 163 | registryKey.SetValue(_autostartName, exePath); 164 | _autostart.Checked = true; 165 | } 166 | else 167 | { 168 | registryKey.DeleteValue(_autostartName); 169 | _autostart.Checked = false; 170 | } 171 | } 172 | catch (Exception ex) 173 | { 174 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 175 | } 176 | } 177 | 178 | private void ExitApplication() 179 | { 180 | _watcher.Dispose(); 181 | var values = _overlayWindows.Values.ToList(); 182 | foreach (var item in values) 183 | { 184 | DestroyOverlay(item.chartView); 185 | } 186 | _overlayWindows.Clear(); 187 | 188 | _notifyIcon.Dispose(); 189 | _notifyIcon = null; 190 | MainWindow.Close(); 191 | } 192 | 193 | private void CreateOverlay(AutomationElement chartView, IntPtr parentOperationStatusWindow) 194 | { 195 | System.Diagnostics.Debug.WriteLine($"Progress chart opened {chartView.Current.NativeWindowHandle}"); 196 | OverlayWindow ow = (OverlayWindow)Activator.CreateInstance(_currentGame); 197 | if (!_overlayWindows.ContainsKey(chartView)) 198 | { 199 | _overlayWindows.Add(chartView, new OverlayWindowData { window = ow, chartView = chartView, parentOperationStatusWindow = parentOperationStatusWindow }); 200 | ow.Wake(chartView, parentOperationStatusWindow); 201 | } 202 | } 203 | 204 | private void DestroyOverlay(AutomationElement chartView) 205 | { 206 | System.Diagnostics.Debug.WriteLine($"Progress chart closed"); 207 | if (_overlayWindows.ContainsKey(chartView)) 208 | { 209 | _overlayWindows[chartView].window.Close(); 210 | _overlayWindows.Remove(chartView); 211 | } 212 | } 213 | 214 | public void OnChartViewOpened(AutomationElement chartView, IntPtr parentOperationStatusWindow) 215 | { 216 | Dispatcher.BeginInvoke(new Action(() => 217 | { 218 | CreateOverlay(chartView, parentOperationStatusWindow); 219 | })); 220 | } 221 | 222 | public void OnChartViewClosed(AutomationElement chartView, IntPtr parentOperationStatusWindow) 223 | { 224 | Dispatcher.BeginInvoke(new Action(() => 225 | { 226 | DestroyOverlay(chartView); 227 | })); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Copy Dialog Lunar Lander.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {C244F3B7-8B79-48FD-AD76-CA5583811D8D} 8 | WinExe 9 | CopyDialogLunarLander 10 | CopyDialogLunarLander 11 | v4.7.1 12 | 512 13 | true 14 | 15 | 16 | 17 | x86 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | true 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | true 36 | 37 | 38 | 39 | 40 | 41 | Source\app.manifest 42 | 43 | 44 | 45 | False 46 | True 47 | packages\UIAComWrapper.1.1.0.14\lib\net40\Interop.UIAutomationClient.dll 48 | 49 | 50 | packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll 51 | 52 | 53 | 54 | 55 | 56 | packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll 57 | 58 | 59 | packages\System.Console.4.3.0\lib\net46\System.Console.dll 60 | 61 | 62 | 63 | 64 | packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll 65 | 66 | 67 | packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll 68 | 69 | 70 | 71 | packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll 72 | 73 | 74 | packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll 75 | 76 | 77 | packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll 78 | 79 | 80 | packages\System.Net.Http.4.3.2\lib\net46\System.Net.Http.dll 81 | 82 | 83 | packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll 84 | 85 | 86 | packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll 87 | 88 | 89 | packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net46\System.Security.Cryptography.Algorithms.dll 90 | 91 | 92 | packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll 93 | 94 | 95 | packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll 96 | 97 | 98 | packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net46\System.Security.Cryptography.X509Certificates.dll 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll 109 | 110 | 111 | False 112 | packages\UIAComWrapper.1.1.0.14\lib\net40\UIAComWrapper.dll 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | True 130 | True 131 | Resources.resx 132 | 133 | 134 | 135 | 136 | Designer 137 | 138 | 139 | 140 | 141 | 142 | 143 | ResXFileCodeGenerator 144 | Resources.Designer.cs 145 | Designer 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | {8a1fce19-725d-4df7-83a4-079752be2e9c} 154 | Box2D.NetStandard 155 | 156 | 157 | 158 | 159 | 160 | Interop.UIAutomationClient.dll 161 | PreserveNewest 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /Source/CopyWatcher/CopyWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Windows.Automation; 8 | 9 | namespace CopyDialogLunarLander 10 | { 11 | /// 12 | /// Detects new copy dialogs being opened and closed as well as new charts added to existing dialogs. 13 | /// 14 | class OperationStatusWindowWatcher : IDisposable 15 | { 16 | public delegate void ChartViewOpenedHandler(AutomationElement chartView, IntPtr parentOperationStatusWindow); 17 | public event ChartViewOpenedHandler ChartViewOpened; 18 | public delegate void ChartViewClosedHandler(AutomationElement chartView, IntPtr parentOperationStatusWindow); 19 | public event ChartViewClosedHandler ChartViewClosed; 20 | 21 | private PropertyCondition _chartViewCondition = new PropertyCondition(AutomationElement.ClassNameProperty, "ChartView", PropertyConditionFlags.IgnoreCase); 22 | private PropertyCondition _operationStatusWindowCondition = new PropertyCondition(AutomationElement.ClassNameProperty, "OperationStatusWindow"); 23 | private AutomationEventHandler _eventHandler; 24 | private Task _backgroundUpdater; 25 | private bool _runBackgroundTask = true; 26 | 27 | class StatusWindow 28 | { 29 | public AutomationElement statusWindow; 30 | public List progressWindows = new List(); 31 | } 32 | 33 | struct ProgressChart 34 | { 35 | public AutomationElement progressChart; 36 | public IntPtr nativeHWND; 37 | } 38 | private Dictionary _trackedWindows = new Dictionary(); 39 | 40 | public OperationStatusWindowWatcher() 41 | { 42 | } 43 | 44 | public void Start() 45 | { 46 | Task.Factory.StartNew(() => FindWindows()); 47 | 48 | { 49 | // Configure automation event handler. 50 | _eventHandler = new AutomationEventHandler(OnWindowOpen); 51 | Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement, TreeScope.Children, _eventHandler); 52 | Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, AutomationElement.RootElement, TreeScope.Children, _eventHandler); 53 | } 54 | 55 | // Start background updater. 56 | _backgroundUpdater = Task.Run(async () => 57 | { 58 | try 59 | { 60 | while (_runBackgroundTask) 61 | { 62 | await Task.Delay(2000); 63 | 64 | if (_runBackgroundTask) 65 | UpdatStatusWindows(); 66 | } 67 | } 68 | catch (Exception ex) 69 | { 70 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 71 | } 72 | 73 | }); 74 | } 75 | 76 | private void FindWindows() 77 | { 78 | AutomationElement rootElement = AutomationElement.RootElement; 79 | AutomationElementCollection elementCollection = rootElement.FindAll(TreeScope.Children, _operationStatusWindowCondition); 80 | foreach (AutomationElement elem in elementCollection) 81 | { 82 | AddStatusWindow(elem); 83 | } 84 | } 85 | 86 | private void AddStatusWindow(AutomationElement statusWindow) 87 | { 88 | lock (_trackedWindows) 89 | { 90 | try 91 | { 92 | var sw = new StatusWindow(); 93 | sw.statusWindow = statusWindow; 94 | IntPtr nativeRoot = new IntPtr((int)statusWindow.Current.NativeWindowHandle); 95 | System.Diagnostics.Debug.WriteLine($"AddStatusWindow: {nativeRoot}"); 96 | AutomationElementCollection progressCharts = statusWindow.FindAll(TreeScope.Subtree, _chartViewCondition); 97 | foreach (AutomationElement pc in progressCharts) 98 | { 99 | sw.progressWindows.Add(new ProgressChart { progressChart = pc, nativeHWND = new IntPtr((int)pc.Current.NativeWindowHandle) }); 100 | ChartViewOpened?.Invoke(pc, nativeRoot); 101 | } 102 | 103 | StatusWindow swOld; 104 | if (_trackedWindows.TryGetValue(nativeRoot, out swOld)) 105 | { 106 | // This can happen as the explorer does not destroy the window but simply hides it when the last operations finishes. 107 | swOld.statusWindow = sw.statusWindow; 108 | swOld.progressWindows.AddRange(sw.progressWindows); 109 | } 110 | else 111 | { 112 | _trackedWindows.Add(nativeRoot, sw); 113 | } 114 | } 115 | catch (Exception ex) 116 | { 117 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 118 | } 119 | } 120 | } 121 | 122 | private void RemoveStatusWindow(AutomationElement statusWindow) 123 | { 124 | lock (_trackedWindows) 125 | { 126 | try 127 | { 128 | IntPtr nativeRoot = new IntPtr((int)statusWindow.Current.NativeWindowHandle); 129 | System.Diagnostics.Debug.WriteLine($"RemoveStatusWindow: {nativeRoot}"); 130 | 131 | StatusWindow sw; 132 | if (_trackedWindows.TryGetValue(nativeRoot, out sw)) 133 | { 134 | foreach (var pw in sw.progressWindows) 135 | { 136 | ChartViewClosed?.Invoke(pw.progressChart, nativeRoot); 137 | } 138 | _trackedWindows.Remove(nativeRoot); 139 | } 140 | } 141 | catch (Exception ex) 142 | { 143 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 144 | } 145 | } 146 | } 147 | 148 | private void UpdatStatusWindows() 149 | { 150 | lock (_trackedWindows) 151 | { 152 | 153 | foreach (var item in _trackedWindows) 154 | { 155 | try 156 | { 157 | List oldCharts = item.Value.progressWindows.Select(x => x.nativeHWND).ToList(); 158 | List newCharts = new List(); 159 | AutomationElementCollection progressCharts = item.Value.statusWindow.FindAll(TreeScope.Subtree, _chartViewCondition); 160 | foreach (AutomationElement pc in progressCharts) 161 | { 162 | IntPtr nativeRoot = new IntPtr((int)pc.Current.NativeWindowHandle); 163 | newCharts.Add(nativeRoot); 164 | } 165 | 166 | // Remove old 167 | var toRemove = item.Value.progressWindows.Where(x => !newCharts.Contains(x.nativeHWND)).ToList(); 168 | foreach (var remove in toRemove) 169 | { 170 | ChartViewClosed?.Invoke(remove.progressChart, item.Key); 171 | } 172 | item.Value.progressWindows.RemoveAll(x => !newCharts.Contains(x.nativeHWND)); 173 | 174 | // Add new 175 | foreach (AutomationElement pc in progressCharts) 176 | { 177 | IntPtr nativeRoot = new IntPtr((int)pc.Current.NativeWindowHandle); 178 | if (!oldCharts.Contains(nativeRoot)) 179 | { 180 | item.Value.progressWindows.Add(new ProgressChart { progressChart = pc, nativeHWND = new IntPtr((int)pc.Current.NativeWindowHandle) }); 181 | ChartViewOpened?.Invoke(pc, item.Key); 182 | } 183 | } 184 | } 185 | catch (Exception ex) 186 | { 187 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 188 | } 189 | } 190 | 191 | } 192 | } 193 | 194 | private void OnWindowOpen(object src, AutomationEventArgs e) 195 | { 196 | // Make sure the element still exists. Elements such as tooltips 197 | // can disappear before the event is processed. 198 | AutomationElement sourceElement; 199 | try 200 | { 201 | sourceElement = src as AutomationElement; 202 | if (sourceElement.Current.ClassName != "OperationStatusWindow") 203 | return; 204 | } 205 | catch (Exception ex) 206 | { 207 | System.Diagnostics.Debug.WriteLine($"Exception: {ex}"); 208 | return; 209 | } 210 | 211 | if (e.EventId == WindowPattern.WindowOpenedEvent) 212 | AddStatusWindow(sourceElement); 213 | else if (e.EventId == WindowPattern.WindowClosedEvent) 214 | RemoveStatusWindow(sourceElement); 215 | } 216 | 217 | #region IDisposable Support 218 | private bool disposedValue = false; // To detect redundant calls 219 | 220 | protected virtual void Dispose(bool disposing) 221 | { 222 | if (!disposedValue) 223 | { 224 | if (disposing) 225 | { 226 | _runBackgroundTask = false; 227 | if (_backgroundUpdater != null) 228 | { 229 | _backgroundUpdater.Wait(); 230 | _backgroundUpdater = null; 231 | } 232 | } 233 | 234 | Automation.RemoveAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement, _eventHandler); 235 | Automation.RemoveAutomationEventHandler(WindowPattern.WindowClosedEvent, AutomationElement.RootElement, _eventHandler); 236 | 237 | disposedValue = true; 238 | } 239 | } 240 | 241 | // todo: override a finalizer only if dispose(bool disposing) above has code to free unmanaged resources. 242 | ~OperationStatusWindowWatcher() 243 | { 244 | // do not change this code. put cleanup code in dispose(bool disposing) above. 245 | Dispose(false); 246 | } 247 | 248 | // This code added to correctly implement the disposable pattern. 249 | public void Dispose() 250 | { 251 | // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 252 | Dispose(true); 253 | GC.SuppressFinalize(this); 254 | } 255 | #endregion 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /Source/GameOverlay/OverlayWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Drawing.Imaging; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Automation; 10 | using System.Windows.Controls; 11 | using System.Windows.Media; 12 | using System.Windows.Threading; 13 | using System.Windows.Interop; 14 | using System.Runtime.InteropServices; 15 | using System.Windows.Input; 16 | using System.Windows.Media.Imaging; 17 | using System.Diagnostics; 18 | using System.ComponentModel; 19 | 20 | namespace CopyDialogLunarLander 21 | { 22 | /// 23 | /// Current frame stats. 24 | /// 25 | public struct OverlayStats 26 | { 27 | public double captureTime; 28 | public double updateTime; 29 | public double frameTimer; 30 | public double fps; 31 | 32 | public string GetStatsString() 33 | { 34 | return $"Capture: {captureTime.ToString("0.00")} Update: {updateTime.ToString("0.00")} FPS: {fps.ToString("00.00")} Frame: {frameTimer.ToString("00.00")}"; 35 | } 36 | } 37 | 38 | 39 | /// 40 | /// Base class for windows attached to a single progress chart in a operation status dialog window. 41 | /// Any new game needs to derive from this class and implement GameInterface. 42 | /// A game can also implement "static void FillOptions(System.Windows.Forms.ToolStripMenuItem optionsMenu)" to add options to the tray icon. 43 | /// 44 | public abstract class OverlayWindow : Window, GameInterface 45 | { 46 | enum State 47 | { 48 | Sleeping, 49 | LookingForChart, 50 | Playing 51 | } 52 | 53 | private AutomationElement _trackedChartView = null; 54 | private IntPtr _parentOperationStatusWindow = IntPtr.Zero; 55 | 56 | private static OverlayWindow _activeOverlayWindow = null; 57 | 58 | private PropertyCondition _progressChartCondition = new PropertyCondition(AutomationElement.ClassNameProperty, "RateChartOverlayWindow", PropertyConditionFlags.None); 59 | private AutomationElement _progressChart = null; 60 | 61 | private State _state = State.Sleeping; 62 | private System.Windows.Rect _rect = new Rect(); 63 | 64 | System.Drawing.Color _terrainColor = System.Drawing.Color.Empty; 65 | DrawingGroup _backingStore = new DrawingGroup(); 66 | 67 | private OverlayStats _stats = new OverlayStats(); 68 | private Stopwatch _fpsStopWatch = null; 69 | private long _frames = 0; 70 | 71 | public OverlayWindow() 72 | { 73 | AllowsTransparency = true; 74 | WindowStyle = WindowStyle.None; 75 | // Alpha needs to be at least 1 or the window input will click through to the underlying window. 76 | Background = new SolidColorBrush(new System.Windows.Media.Color() { R = 0, G = 0, B = 0, A = 1 }); 77 | Topmost = true; 78 | ShowInTaskbar = false; 79 | //RenderOptions.SetEdgeMode(this, EdgeMode.Aliased); 80 | 81 | Width = 200; 82 | Height = 100; 83 | Left = 100; 84 | Top = 100; 85 | 86 | _stats.fps = 30.0f; 87 | } 88 | 89 | #region GameInterface 90 | 91 | public abstract void Init(System.Windows.Size worldSize); 92 | public abstract void DeInit(); 93 | public abstract void HeightFieldUpdated(float[] heightField, System.Drawing.Color terrainColor); 94 | public abstract void Update(DrawingGroup backingStore, OverlayStats stats); 95 | 96 | #endregion GameInterface 97 | 98 | #region Private functions 99 | 100 | private void CompositionTarget_Rendering(object sender, EventArgs e) 101 | { 102 | Stopwatch sw = Stopwatch.StartNew(); 103 | try 104 | { 105 | TrackChartView(); 106 | } 107 | catch (System.Windows.Automation.ElementNotAvailableException) 108 | { 109 | Sleep(); 110 | } 111 | _stats.frameTimer = sw.ElapsedMilliseconds; 112 | } 113 | 114 | public void Wake(AutomationElement trackedCharView, IntPtr parentOperationStatusWindow) 115 | { 116 | if (_state != State.Sleeping) 117 | return; 118 | 119 | _fpsStopWatch = Stopwatch.StartNew(); 120 | _state = State.LookingForChart; 121 | this._trackedChartView = trackedCharView; 122 | this._parentOperationStatusWindow = parentOperationStatusWindow; 123 | 124 | var hwnd = new IntPtr(trackedCharView.Current.NativeWindowHandle); 125 | uint dpi = NativeInterop.GetDpiForWindow(hwnd); 126 | // BoundingRectangle is in physical device coordinates (pixels), we need to convert to logical units first. 127 | _rect = _trackedChartView.Current.BoundingRectangle; 128 | _rect = new Rect(0, 0, Math.Ceiling(_rect.Width / dpi * 96), Math.Ceiling(_rect.Height / dpi * 96)); 129 | // This should be equivalent to these device independent constants but not sure if that is true for all OS versions. 130 | // _rect = new Rect(0, 0, 395.0, 85.0); 131 | Init(new System.Windows.Size(_rect.Width, _rect.Height)); 132 | 133 | try 134 | { 135 | TrackChartView(); 136 | CompositionTarget.Rendering += CompositionTarget_Rendering; 137 | } 138 | catch (System.Windows.Automation.ElementNotAvailableException) 139 | { 140 | Sleep(); 141 | } 142 | } 143 | 144 | 145 | public void Sleep() 146 | { 147 | CompositionTarget.Rendering -= CompositionTarget_Rendering; 148 | _state = State.Sleeping; 149 | Hide(); 150 | } 151 | 152 | protected override void OnClosing(CancelEventArgs e) 153 | { 154 | if (_activeOverlayWindow == this) 155 | { 156 | _activeOverlayWindow = null; 157 | } 158 | DeInit(); 159 | Sleep(); 160 | base.OnClosing(e); 161 | } 162 | 163 | public void TrackChartView() 164 | { 165 | if (_trackedChartView == null) 166 | { 167 | Sleep(); 168 | return; 169 | } 170 | 171 | if (_progressChart == null) 172 | { 173 | _progressChart = _trackedChartView.FindFirst(TreeScope.Descendants, _progressChartCondition); 174 | } 175 | // The progress chart can be minimized by clicking 'fewer details'. The progress chart itself shouldn't be used for 176 | // anything else as any queries on it glitches out windows automation and pure win32 API calls. 177 | if (_progressChart != null && NativeInterop.IsWindowVisible(new IntPtr(_progressChart.Current.NativeWindowHandle))) 178 | { 179 | _state = State.Playing; 180 | 181 | Show(); 182 | 183 | bool showOverlay = false; 184 | try 185 | { 186 | IntPtr hwndFocus = NativeInterop.GetForegroundWindow(); 187 | IntPtr hwndCurrent = new IntPtr((int)_trackedChartView.Current.NativeWindowHandle); 188 | var wih = new System.Windows.Interop.WindowInteropHelper(this); 189 | IntPtr hwndThis = wih.Handle; 190 | // IsActive function is unreliable for always in foreground windows so we compute it manually. 191 | bool isActive = hwndThis == hwndFocus; 192 | 193 | if (isActive && _activeOverlayWindow != this) 194 | { 195 | _activeOverlayWindow = this; 196 | System.Diagnostics.Debug.WriteLine($"Active: {hwndCurrent}, parent OW: {_parentOperationStatusWindow}"); 197 | } 198 | else if (!isActive && _activeOverlayWindow == this) 199 | { 200 | _activeOverlayWindow = null; 201 | System.Diagnostics.Debug.WriteLine($"Not Active: {hwndCurrent}, parent OW: {_parentOperationStatusWindow}"); 202 | } 203 | if (_activeOverlayWindow != null && _activeOverlayWindow._parentOperationStatusWindow == _parentOperationStatusWindow) 204 | { 205 | showOverlay = true; 206 | } 207 | 208 | while (hwndCurrent != IntPtr.Zero) 209 | { 210 | if (hwndCurrent == hwndFocus) 211 | { 212 | showOverlay = true; 213 | break; 214 | } 215 | hwndCurrent = NativeInterop.GetParent(hwndCurrent); 216 | } 217 | 218 | showOverlay = showOverlay || isActive; 219 | } 220 | catch (Exception) 221 | { 222 | } 223 | 224 | { 225 | // BoundingRectangle is in physical device coordinates (pixel) units. Convert to logical. 226 | var rect = _trackedChartView.Current.BoundingRectangle; 227 | var pos = this.PointFromScreen(new System.Windows.Point(rect.Left, rect.Top)); 228 | var pos2 = this.PointFromScreen(new System.Windows.Point(rect.Right, rect.Bottom)); 229 | // Convert to absolute coordinates by adding the current pos to them. 230 | pos.X += Left; 231 | pos.Y += Top; 232 | pos2.X += Left; 233 | pos2.Y += Top; 234 | 235 | Width = pos2.X - pos.X; 236 | Height = pos2.Y - pos.Y; 237 | Left = pos.X; 238 | // Hacky way of moving the overlay out of the way. 239 | Top = showOverlay ? pos.Y : 20000 ; 240 | } 241 | 242 | { 243 | Stopwatch sw = Stopwatch.StartNew(); 244 | UpdateHeightfield(); 245 | _stats.captureTime = sw.Elapsed.TotalMilliseconds; 246 | 247 | sw.Restart(); 248 | Update(_backingStore, _stats); 249 | _stats.updateTime = sw.Elapsed.TotalMilliseconds; 250 | 251 | _frames++; 252 | if (_fpsStopWatch.Elapsed.TotalMilliseconds > 1000) 253 | { 254 | _stats.fps = (double)_frames / ((double)_fpsStopWatch.ElapsedMilliseconds / 1000.0); 255 | _stats.fps = Math.Max(_stats.fps, 30); // Do not fall below 30fps for sim stability, will cause slowdown instead 256 | _fpsStopWatch.Restart(); 257 | _frames = 0; 258 | } 259 | } 260 | } 261 | else 262 | { 263 | _state = State.LookingForChart; 264 | Hide(); 265 | } 266 | } 267 | protected override void OnRender(System.Windows.Media.DrawingContext drawingContext) 268 | { 269 | base.OnRender(drawingContext); 270 | drawingContext.DrawDrawing(_backingStore); 271 | } 272 | 273 | private void UpdateHeightfield() 274 | { 275 | if (_rect.Width == 0) 276 | return; 277 | 278 | _terrainColor = System.Drawing.Color.Empty; 279 | var value = _trackedChartView.GetCurrentPropertyValue(AutomationElement.NativeWindowHandleProperty); 280 | IntPtr nativeHandle = new IntPtr((int)value); 281 | Bitmap bmp = WindowCapture.Capture(nativeHandle); 282 | if (bmp != null) 283 | { 284 | BitmapData pixelData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); 285 | 286 | float[] heightField = new float[(int)_rect.Width]; 287 | //try 288 | //{ 289 | // bmp.Save("D:\\aaa.png"); 290 | //} 291 | //catch (Exception) 292 | //{ 293 | //} 294 | 295 | Dictionary accu = new Dictionary(); 296 | accu[System.Drawing.Color.Empty] = 0; 297 | unsafe 298 | { 299 | // Find the terrain color by searching for all color above a certain threshold of 300 | // saturation and then picking the most common one. 301 | byte* pixelPtr = (byte*)pixelData.Scan0; 302 | for (int x = 2; x < 6; x++) 303 | { 304 | for (int y = 0; y < pixelData.Height; ++y) 305 | { 306 | byte* pixel = pixelPtr + (x * 4 + y * pixelData.Stride); 307 | byte b = pixel[0]; 308 | byte g = pixel[1]; 309 | byte r = pixel[2]; 310 | if (r == g && r == b) 311 | continue; 312 | 313 | System.Drawing.Color color = System.Drawing.Color.FromArgb(r, g, b); 314 | float sat = color.GetSaturation(); 315 | if (sat > 0.8) 316 | { 317 | if (!accu.ContainsKey(color)) 318 | accu[color] = 0; 319 | accu[color]++; 320 | } 321 | } 322 | } 323 | _terrainColor = accu.Aggregate((l, r) => l.Value > r.Value ? l : r).Key; 324 | 325 | // Find where the terrain color stops, starting at the bottom of each column of pixels. 326 | if (_terrainColor != System.Drawing.Color.Empty) 327 | { 328 | Parallel.For(0, (int)_rect.Width, x => 329 | { 330 | heightField[x] = 0; 331 | bool terrainStarted = false; 332 | int pixelPosX = (int)Math.Round(((double)x / _rect.Width) * pixelData.Width); 333 | for (int y = pixelData.Height - 1; y >= 0; --y) 334 | { 335 | byte* pixel = pixelPtr + (pixelPosX * 4 + y * pixelData.Stride); 336 | byte b = pixel[0]; 337 | byte g = pixel[1]; 338 | byte r = pixel[2]; 339 | 340 | System.Drawing.Color color = System.Drawing.Color.FromArgb(r, g, b); 341 | if (color == System.Drawing.Color.FromArgb(0, 0, 0)) 342 | continue; 343 | if (_terrainColor == color) 344 | { 345 | terrainStarted = true; 346 | } 347 | else if (_terrainColor != color && terrainStarted) 348 | { 349 | // Only pick the terrain height if we actually encountered a terrain pixel. 350 | // This is needed because on some systems there is a while line randomly at 351 | // the bottom which would otherwise stop traversal. 352 | heightField[x] = (float)(pixelData.Height - y - 1) / pixelData.Height; 353 | break; 354 | } 355 | else if (!terrainStarted && (pixelData.Height - y) > 5) 356 | { 357 | // If we didn't encounter a single terrain pixel in the bottom 5 pixels of a column, 358 | // there probably is no terrain here and we can stop searching. 359 | break; 360 | } 361 | } 362 | }); 363 | } 364 | } 365 | bmp.UnlockBits(pixelData); 366 | 367 | if (_terrainColor != System.Drawing.Color.Empty) 368 | { 369 | HeightFieldUpdated(heightField, _terrainColor); 370 | } 371 | 372 | } 373 | } 374 | #endregion Private functions 375 | 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /Source/LunarLander/LunarSim.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Box2DX.Common; 7 | using Box2DX.Collision; 8 | using Box2DX.Dynamics; 9 | 10 | namespace CopyDialogLunarLander 11 | { 12 | [FlagsAttribute] 13 | enum GameState 14 | { 15 | None = 0, 16 | Active = 1, 17 | Playing = 4, 18 | } 19 | 20 | enum ShapeType 21 | { 22 | Ground, 23 | Debris, 24 | LanderCockpit, 25 | LanderFoundation, 26 | LanderLeftLeg, 27 | LanderRightLeg 28 | } 29 | 30 | struct GameStats 31 | { 32 | public void Init() 33 | { 34 | Dead = false; 35 | Success = false; 36 | TargetLocation = new Vec2(0, 0); 37 | CurrentLocation = new Vec2(0, 0); 38 | Score = 0; 39 | 40 | Fuel = 10000; 41 | Speed = 0; 42 | LastStep = 0; 43 | WorldTime = 0.0f; 44 | LeftLegOnGroud = false; 45 | RightLegOnGroud = false; 46 | } 47 | 48 | public int GetScore() 49 | { 50 | return (int)System.Math.Round((Fuel / 10000.0f) * System.Math.Max(1.0f - System.Math.Abs(TargetLocation.X - CurrentLocation.X) / 100.0f, 0.0f) * 1000.0f); 51 | } 52 | 53 | 54 | public bool Dead; 55 | public bool Success; 56 | public Vec2 TargetLocation; 57 | public Vec2 CurrentLocation; 58 | public float Score; 59 | 60 | public float Fuel; 61 | public float Speed; 62 | 63 | public float LastStep; 64 | public float WorldTime; 65 | 66 | public bool LeftLegOnGroud; 67 | public bool RightLegOnGroud; 68 | } 69 | 70 | class LunarSim : IDisposable, ContactListener 71 | { 72 | class TerrainBlock 73 | { 74 | public TerrainBlock(int blockIndex, int blockSize) 75 | { 76 | _blockIndex = blockIndex; 77 | _blockSize = blockSize; 78 | } 79 | 80 | public void Clear(World world) 81 | { 82 | _poi = new Vec2(0,0); 83 | if (_bodies != null) 84 | { 85 | _bodies.ToList().ForEach(s => { world.DestroyBody(s); }); 86 | _bodies = null; 87 | } 88 | } 89 | 90 | public void Update(World world, AABB _worldAABB, IList segment) 91 | { 92 | Clear(world); 93 | 94 | List vertices = new List(segment.Count); 95 | for (int i = 0; i < segment.Count; ++i) 96 | { 97 | vertices.Add(new Vec2(_blockIndex * _blockSize + i, _worldAABB.Extents.Y * 2 * segment[i])); 98 | } 99 | 100 | for (int i = vertices.Count - 2; i > 0; --i) 101 | { 102 | float middle = (vertices[i - 1].Y + vertices[i + 1].Y) / 2.0f; 103 | if (System.Math.Abs(middle - vertices[i].Y) < 0.05f) 104 | { 105 | vertices.RemoveAt(i); 106 | } 107 | } 108 | 109 | _bodies = new Body[vertices.Count - 1]; 110 | _poi = vertices[0]; 111 | for (int i = 0; i < vertices.Count - 1; ++i) 112 | { 113 | EdgeDef sd = new EdgeDef(); 114 | sd.Vertex1 = vertices[i]; 115 | sd.Vertex2 = vertices[i + 1]; 116 | sd.Friction = 1; 117 | sd.UserData = ShapeType.Ground; 118 | BodyDef bd = new BodyDef(); 119 | bd.Position.Set(0.0f, 0); 120 | bd.UserData = ShapeType.Ground; 121 | 122 | _bodies[i] = world.CreateBody(bd); 123 | _bodies[i].CreateFixture(sd); 124 | } 125 | } 126 | 127 | public Vec2 GetPointOfInterest() 128 | { 129 | return _poi; 130 | } 131 | 132 | public Body[] _bodies; 133 | int _blockIndex; 134 | int _blockSize; 135 | Vec2 _poi; 136 | } 137 | 138 | public class Debris 139 | { 140 | public Debris(World world, Body body, float lifetime) 141 | { 142 | _world = world; 143 | _body = body; 144 | _lifetime = lifetime; 145 | } 146 | 147 | public bool Step(float dt) 148 | { 149 | _lifetime -= dt; 150 | if (_lifetime < 0) 151 | { 152 | _world.DestroyBody(_body); 153 | _body = null; 154 | return true; 155 | } 156 | return false; 157 | } 158 | 159 | World _world; 160 | Body _body; 161 | float _lifetime; 162 | } 163 | 164 | public static float k_maxSpeed = 5; 165 | 166 | private Random _rnd = new Random(); 167 | 168 | private AABB _worldAABB; 169 | private GameState _state = GameState.None; 170 | private GameStats _stats; 171 | 172 | private bool _debugMode = false; 173 | private LunarDraw _draw = new LunarDraw(); 174 | private LunarDebugSceneDraw _debugSceneDraw = new LunarDebugSceneDraw(); 175 | private LunarSceneDraw _lunarSceneDraw = new LunarSceneDraw(); 176 | private World _world; 177 | private float[] _heightField = null; 178 | System.Drawing.Color _terrainColor = System.Drawing.Color.Empty; 179 | 180 | 181 | private TerrainBlock[] _terrain = null; 182 | private int _lastBlockWithData = 0; 183 | 184 | private Body _lander = null; 185 | private List _debris = new List(); 186 | 187 | private bool _left = false; 188 | private bool _right = false; 189 | private bool _down = false; 190 | 191 | public LunarSim() 192 | { 193 | } 194 | 195 | public void Dispose() 196 | { 197 | Dispose(true); 198 | } 199 | 200 | protected virtual void Dispose(bool state) 201 | { 202 | if (state) 203 | { 204 | // By deleting the world, we delete the bomb, mouse joint, etc. 205 | _world.Dispose(); 206 | _world = null; 207 | } 208 | } 209 | 210 | public void Init(System.Windows.Size worldSize) 211 | { 212 | _stats.Init(); 213 | _state &= ~GameState.Playing; 214 | _debugSceneDraw.Flags = DebugDraw.DrawFlags.Shape; 215 | _lunarSceneDraw.Flags = DebugDraw.DrawFlags.Shape; 216 | _worldAABB = new AABB(); 217 | _worldAABB.LowerBound.Set(0, 0); 218 | _worldAABB.UpperBound.Set((float)worldSize.Width, (float)worldSize.Height); 219 | Vec2 gravity = new Vec2(); 220 | gravity.Set(0.0f, -9.81f); 221 | bool doSleep = false; 222 | 223 | AABB simAABB = _worldAABB; 224 | simAABB.LowerBound.X = -100; 225 | simAABB.UpperBound.X += 200; 226 | simAABB.UpperBound.Y += 200; 227 | _world = new World(simAABB, gravity, doSleep); 228 | _world.SetDebugDraw(_lunarSceneDraw); 229 | _world.SetContactListener(this); 230 | { 231 | // Ground 232 | PolygonDef sd = new PolygonDef(); 233 | sd.UserData = ShapeType.Ground; 234 | sd.SetAsBox((float)worldSize.Width, 1); 235 | 236 | BodyDef bd = new BodyDef(); 237 | bd.Position.Set(0.0f, 0); 238 | bd.UserData = ShapeType.Ground; 239 | Body ground = _world.CreateBody(bd); 240 | ground.CreateFixture(sd); 241 | } 242 | } 243 | 244 | private Body CreateLander(Vec2 pos) 245 | { 246 | float friction = 1; 247 | // Lander 248 | PolygonDef sd = new PolygonDef(); 249 | sd.SetAsBox(5, 1, new Vec2(0, -3f), 0); 250 | sd.Density = 5.0f; 251 | sd.Friction = friction; 252 | 253 | // Foundation 254 | BodyDef bd = new BodyDef(); 255 | bd.AngularDamping = 0.5f; 256 | bd.Position = pos; 257 | bd.UserData = ShapeType.LanderFoundation; 258 | Body lander = _world.CreateBody(bd); 259 | lander.CreateFixture(sd); 260 | 261 | { 262 | // Cockpit 263 | CircleDef CapsuleDef = new CircleDef(); 264 | CapsuleDef.LocalPosition = new Vec2(0, 2f); 265 | CapsuleDef.Radius = 4; 266 | CapsuleDef.Density = 5.0f; 267 | CapsuleDef.Friction = friction; 268 | CapsuleDef.UserData = ShapeType.LanderCockpit; 269 | lander.CreateFixture(CapsuleDef); 270 | } 271 | { 272 | // Left leg 273 | PolygonDef legDef = new PolygonDef(); 274 | legDef.SetAsBox(1, 2, new Vec2(-5f, -6f), 0.0f); 275 | legDef.Density = 2.0f; 276 | legDef.Friction = friction; 277 | legDef.UserData = ShapeType.LanderLeftLeg; 278 | lander.CreateFixture(legDef); 279 | } 280 | { 281 | // Right leg 282 | PolygonDef legDef = new PolygonDef(); 283 | legDef.SetAsBox(1, 2, new Vec2(5f, -6f), 0.0f); 284 | legDef.Density = 2.0f; 285 | legDef.Friction = friction; 286 | legDef.UserData = ShapeType.LanderRightLeg; 287 | lander.CreateFixture(legDef); 288 | } 289 | lander.SetMassFromShapes(); 290 | return lander; 291 | } 292 | 293 | void ExplodeLander() 294 | { 295 | if (_lander != null) 296 | { 297 | Vec2 pos = _stats.CurrentLocation; 298 | int maxOffset = 10; 299 | _world.DestroyBody(_lander); 300 | _lander = null; 301 | _state &= ~GameState.Playing; 302 | 303 | Vec2[] BoxSizes = new Vec2[3]; 304 | BoxSizes[0] = new Vec2(1, 2); 305 | BoxSizes[1] = new Vec2(1, 2); 306 | BoxSizes[2] = new Vec2(5, 1); 307 | 308 | for (int i = 0; i < 3; i++) 309 | { 310 | int force = 6000; 311 | Vec2 offset = new Vec2(_rnd.Next(-maxOffset, maxOffset), _rnd.Next(0, maxOffset)); 312 | var box = AddBox(new System.Windows.Point(pos.X + offset.X, pos.Y + offset.Y), BoxSizes[i].X, BoxSizes[i].Y); 313 | _debris.Add(new Debris(_world, box, 5 + 0.1f * _rnd.Next(-10, 10))); 314 | box.ApplyImpulse(new Vec2(_rnd.Next(-force, force), _rnd.Next(-force, force)), pos); 315 | } 316 | 317 | for (int i = 0; i < 7; i++) 318 | { 319 | int force = 6000; 320 | Vec2 offset = new Vec2(_rnd.Next(-maxOffset, maxOffset), _rnd.Next(0, maxOffset)); 321 | var box = AddBox(new System.Windows.Point(pos.X + offset.X, pos.Y + offset.Y)); 322 | _debris.Add(new Debris(_world, box, 5 + 0.1f * _rnd.Next(-10, 10))); 323 | box.ApplyImpulse(new Vec2(_rnd.Next(-force, force), _rnd.Next(-force, force)), pos); 324 | } 325 | } 326 | } 327 | 328 | void WinTheGame() 329 | { 330 | if (!_stats.Success && _lander != null) 331 | { 332 | _stats.Success = true; 333 | _lander.SetStatic(); 334 | _state &= ~GameState.Playing; 335 | } 336 | } 337 | 338 | public Body Lander 339 | { 340 | get { return _lander; } 341 | } 342 | 343 | public GameState State 344 | { 345 | get { return _state; } 346 | } 347 | 348 | public GameStats Stats 349 | { 350 | get { return _stats; } 351 | } 352 | 353 | public void SetDebug(bool debug) 354 | { 355 | _debugMode = debug; 356 | } 357 | 358 | public void SetActive(bool active) 359 | { 360 | if (active) 361 | _state |= GameState.Active; 362 | else 363 | _state &= ~GameState.Active; 364 | } 365 | 366 | public void SetHeightField(float[] heightField, System.Drawing.Color terrainColor) 367 | { 368 | _terrainColor = terrainColor; 369 | if (_heightField == null) 370 | { 371 | _heightField = new float[heightField.Count()]; 372 | } 373 | 374 | int blockSize = 5; 375 | int blocks = (heightField.Count() - 1) / blockSize; // Minus 1 so we can always access the next block's first element. 376 | if (_terrain == null || _terrain.Count() != blocks) 377 | { 378 | if (_terrain != null) 379 | { 380 | _terrain.ToList().ForEach(s => { s.Clear(_world); }); 381 | } 382 | _terrain = new TerrainBlock[blocks]; 383 | for (int i = 0; i < blocks; i++) 384 | _terrain[i] = new TerrainBlock(i, blockSize); 385 | } 386 | 387 | for (int i = 0; i < blocks; ++i) 388 | { 389 | bool dirty = false; 390 | // Each block ends where the next starts so the block actually contains blockSize + 1 vertices or blockSize edges. 391 | for (int p = 0; p <= blockSize; ++p) 392 | { 393 | if (System.Math.Abs(_heightField[i * blockSize + p] - heightField[i * blockSize + p]) > 0.001) 394 | { 395 | dirty = true; 396 | _lastBlockWithData = i; 397 | break; 398 | } 399 | } 400 | 401 | if (dirty) 402 | { 403 | _terrain[i].Update(_world, _worldAABB, new ArraySegment(heightField, i * blockSize, blockSize + 1)); 404 | } 405 | } 406 | 407 | _heightField = heightField; 408 | } 409 | 410 | public Body AddBox(System.Windows.Point p, float width = 2.5f, float height = 2.5f) 411 | { 412 | PolygonDef sd = new PolygonDef(); 413 | sd.SetAsBox(width, height); 414 | sd.Density = 5.0f; 415 | sd.UserData = ShapeType.Debris; 416 | 417 | Vec2 pos = new Vec2((float)p.X, (float)p.Y); 418 | 419 | BodyDef bd = new BodyDef(); 420 | bd.Position = pos; 421 | bd.UserData = ShapeType.Debris; 422 | Body body = _world.CreateBody(bd); 423 | body.CreateFixture(sd); 424 | body.SetMassFromShapes(); 425 | return body; 426 | } 427 | 428 | public void Input(bool left, bool right, bool down) 429 | { 430 | if (!_state.HasFlag(GameState.Active) || _lander == null && _stats.Fuel > 0 || _stats.Dead || _stats.Success || _stats.Fuel <= 0) 431 | { 432 | _left = false; 433 | _right = false; 434 | _down = false; 435 | return; 436 | } 437 | _left = left; 438 | _right = right; 439 | _down = down; 440 | 441 | float forceUp = 6000.0f; 442 | float forceSide = 1000.0f; 443 | if (down) 444 | { 445 | var downVec = _lander.GetWorldVector(new Vec2(0, 1)); 446 | var center = _lander.GetLocalCenter() + _lander.GetPosition(); 447 | _lander.ApplyForce(downVec * forceUp, center); 448 | _stats.Fuel = System.Math.Max(0, _stats.Fuel - _stats.LastStep * 100); 449 | 450 | 451 | } 452 | 453 | if (left || right) 454 | { 455 | var center = _lander.GetLocalCenter() + _lander.GetPosition(); 456 | center.Y += 5.0f; 457 | float force = forceSide; 458 | if (left) 459 | force = -force; 460 | var forceVec = _lander.GetWorldVector(new Vec2(1, 0)); 461 | _lander.ApplyForce(new Vec2(force, 0), center); 462 | _stats.Fuel = System.Math.Max(0, _stats.Fuel - _stats.LastStep * 25); 463 | } 464 | } 465 | 466 | public void Reset() 467 | { 468 | if (_terrain == null) 469 | { 470 | return; 471 | } 472 | if (_lander != null) 473 | { 474 | _world.DestroyBody(_lander); 475 | } 476 | int startIndex = _rnd.Next(0, _lastBlockWithData); 477 | _lander = CreateLander(new Vec2(_terrain[startIndex].GetPointOfInterest().X, 90f)); 478 | 479 | _stats.Init(); 480 | int targetIndex = _rnd.Next(0, _lastBlockWithData); 481 | _stats.TargetLocation = _terrain[targetIndex].GetPointOfInterest(); 482 | _state |= GameState.Playing; 483 | } 484 | 485 | public void Step(float step, System.Windows.Media.DrawingContext drawingContext) 486 | { 487 | if (!_state.HasFlag(GameState.Active)) 488 | return; 489 | 490 | _lunarSceneDraw.DrawingContext = drawingContext; 491 | _debugSceneDraw.DrawingContext = drawingContext; 492 | _draw.DrawingContext = drawingContext; 493 | 494 | _lunarSceneDraw.OverrideColor = new Box2DX.Dynamics.Color(_terrainColor.R / 255.0f, _terrainColor.G / 255.0f, _terrainColor.B / 255.0f); 495 | _world.SetDebugDraw(_debugMode ? (DebugDraw)_debugSceneDraw : (DebugDraw)_lunarSceneDraw); 496 | 497 | _world.SetWarmStarting(true); 498 | _world.SetContinuousPhysics(true); 499 | 500 | _world.Step(step, 10, 8); 501 | 502 | _world.Validate(); 503 | 504 | _stats.LastStep = step; 505 | _stats.Speed = Lander != null ? Lander.GetLinearVelocity().Length() : 0; 506 | _stats.WorldTime += step; 507 | 508 | 509 | for (int i = _debris.Count - 1; i >= 0; i--) 510 | { 511 | if (_debris[i].Step(step)) 512 | { 513 | _debris.RemoveAt(i); 514 | } 515 | } 516 | 517 | // Evaluate game state. 518 | if (_stats.Dead) 519 | { 520 | ExplodeLander(); 521 | } 522 | else 523 | { 524 | if (!_stats.Dead && (_state & GameState.Playing) != 0) 525 | { 526 | // Draw target location marker 527 | float width = (float)System.Math.Sin(_stats.WorldTime); 528 | Vec2[] points = new Vec2[3]; 529 | points[0] = _stats.TargetLocation; 530 | points[1] = _stats.TargetLocation; 531 | points[1].X += 10 * width; 532 | points[1].Y += 10; 533 | points[2] = _stats.TargetLocation; 534 | points[2].X -= 10 * width; 535 | points[2].Y += 10; 536 | _draw.DrawPolygon(points, 3, new Box2DX.Dynamics.Color(_terrainColor.R / 255.0f, _terrainColor.G / 255.0f, _terrainColor.B / 255.0f)); 537 | } 538 | 539 | if (_lander != null) 540 | { 541 | _stats.CurrentLocation = _lander.GetPosition(); 542 | 543 | Func CreateThruster = transform => 544 | { 545 | float width = (float)System.Math.Sin(_stats.WorldTime * 100); 546 | Vec2[] points = new Vec2[3]; 547 | points[0] = new Vec2(0, 0); 548 | points[1] = new Vec2(0, 0); 549 | points[1].X += 2 * width; 550 | points[1].Y -= 10; 551 | points[2] = new Vec2(0, 0); 552 | points[2].X -= 2 * width; 553 | points[2].Y -= 10; 554 | for (int i = 0; i < points.Length; i++) 555 | { 556 | points[i] = transform.Position + Box2DX.Common.Math.Mul(transform.R, points[i]); 557 | } 558 | return points; 559 | }; 560 | 561 | if (_down) 562 | { 563 | XForm transform = new XForm(_lander.GetWorldPoint(new Vec2(0, -5)), _lander.GetXForm().R); 564 | Vec2[] points = CreateThruster(transform); 565 | _draw.DrawPolygon(points, 3, new Box2DX.Dynamics.Color(_terrainColor.R / 255.0f, _terrainColor.G / 255.0f, _terrainColor.B / 255.0f)); 566 | } 567 | if (_left) 568 | { 569 | XForm transform = new XForm(_lander.GetWorldPoint(new Vec2(3, 2)), new Mat22(_lander.GetAngle() + 90.0f * (float)System.Math.PI / 180.0f)); 570 | Vec2[] points = CreateThruster(transform); 571 | _draw.DrawPolygon(points, 3, new Box2DX.Dynamics.Color(_terrainColor.R / 255.0f, _terrainColor.G / 255.0f, _terrainColor.B / 255.0f)); 572 | } 573 | if (_right) 574 | { 575 | XForm transform = new XForm(_lander.GetWorldPoint(new Vec2(-3, 2)), new Mat22(_lander.GetAngle() - 90.0f * (float)System.Math.PI / 180.0f)); 576 | Vec2[] points = CreateThruster(transform); 577 | _draw.DrawPolygon(points, 3, new Box2DX.Dynamics.Color(_terrainColor.R / 255.0f, _terrainColor.G / 255.0f, _terrainColor.B / 255.0f)); 578 | } 579 | } 580 | if (_stats.LeftLegOnGroud && _stats.RightLegOnGroud) 581 | { 582 | WinTheGame(); 583 | } 584 | } 585 | } 586 | 587 | public void Render(System.Windows.Media.DrawingContext drawingContext) 588 | { 589 | if ((_state & GameState.Active) != 0) 590 | { 591 | 592 | if ((_state & GameState.Playing) == 0) 593 | { 594 | if (_stats.Success) 595 | { 596 | int score = _stats.GetScore(); 597 | string text; 598 | if (score > 0) 599 | { 600 | text = $"You Won! You score is: {score}"; 601 | } 602 | else 603 | { 604 | text = $"Game Over! You were too far away from your target!"; 605 | } 606 | LunarSceneDraw.DrawText(drawingContext, text, new System.Windows.Point(3, 3), 12D, _terrainColor); 607 | } 608 | else if (_stats.Dead) 609 | { 610 | string text = "Game Over! Your lander exploded!"; 611 | LunarSceneDraw.DrawText(drawingContext, text, new System.Windows.Point(3, 3), 12D, _terrainColor); 612 | } 613 | 614 | string text2 = "restart = 'Space', thrusters = 'arrow keys'"; 615 | LunarSceneDraw.DrawText(drawingContext, text2, new System.Windows.Point(3, 16), 12D, _terrainColor); 616 | } 617 | else 618 | { 619 | string text = "Speed: " + (_stats.Speed > 0 ? "+" : "-") + System.Math.Abs(_stats.Speed).ToString("0.00 m/s") + " Fuel: " + _stats.Fuel.ToString("00000.0 L"); 620 | LunarSceneDraw.DrawText(drawingContext, text, new System.Windows.Point(3, 3), 12D, _terrainColor); 621 | } 622 | } 623 | else 624 | { 625 | string text = "Paused - click here to play"; 626 | LunarSceneDraw.DrawText(drawingContext, text, new System.Windows.Point(6, 6), 15D, _terrainColor); 627 | } 628 | } 629 | 630 | public void BeginContact(Contact contact) 631 | { 632 | if (contact.FixtureA?.UserData is ShapeType && contact.FixtureB?.UserData is ShapeType && !_stats.Dead && !_stats.Success) 633 | { 634 | ShapeType shapeA = (ShapeType)contact.FixtureA.UserData; 635 | ShapeType shapeB = (ShapeType)contact.FixtureB.UserData; 636 | if (shapeA == ShapeType.Ground || shapeA == ShapeType.Debris) 637 | { 638 | var temp = shapeA; 639 | shapeA = shapeB; 640 | shapeB = temp; 641 | } 642 | 643 | switch (shapeA) 644 | { 645 | case ShapeType.LanderCockpit: 646 | case ShapeType.LanderFoundation: 647 | { 648 | _stats.Dead = true; 649 | } 650 | break; 651 | case ShapeType.LanderLeftLeg: 652 | { 653 | if (System.Math.Abs(_stats.Speed) > k_maxSpeed) 654 | { 655 | _stats.Dead = true; 656 | } 657 | else 658 | { 659 | _stats.LeftLegOnGroud = true; 660 | } 661 | } 662 | break; 663 | case ShapeType.LanderRightLeg: 664 | { 665 | if (System.Math.Abs(_stats.Speed) > k_maxSpeed) 666 | { 667 | _stats.Dead = true; 668 | } 669 | else 670 | { 671 | _stats.RightLegOnGroud = true; 672 | } 673 | } 674 | break; 675 | default: 676 | break; 677 | } 678 | } 679 | } 680 | 681 | public void EndContact(Contact contact) 682 | { 683 | if (contact.FixtureA?.UserData is ShapeType && contact.FixtureB?.UserData is ShapeType && !_stats.Dead && !_stats.Success) 684 | { 685 | ShapeType shapeA = (ShapeType)contact.FixtureA.UserData; 686 | ShapeType shapeB = (ShapeType)contact.FixtureB.UserData; 687 | if (shapeA == ShapeType.Ground || shapeA == ShapeType.Debris) 688 | { 689 | var temp = shapeA; 690 | shapeA = shapeB; 691 | shapeB = temp; 692 | } 693 | 694 | switch (shapeA) 695 | { 696 | case ShapeType.LanderCockpit: 697 | case ShapeType.LanderFoundation: 698 | { 699 | _stats.Dead = true; 700 | } 701 | break; 702 | case ShapeType.LanderLeftLeg: 703 | { 704 | if (System.Math.Abs(_stats.Speed) > k_maxSpeed) 705 | { 706 | _stats.Dead = true; 707 | } 708 | else 709 | { 710 | _stats.LeftLegOnGroud = false; 711 | } 712 | } 713 | break; 714 | case ShapeType.LanderRightLeg: 715 | { 716 | if (System.Math.Abs(_stats.Speed) > k_maxSpeed) 717 | { 718 | _stats.Dead = true; 719 | } 720 | else 721 | { 722 | _stats.RightLegOnGroud = false; 723 | } 724 | } 725 | break; 726 | default: 727 | break; 728 | } 729 | } 730 | } 731 | 732 | public void PreSolve(Contact contact, Manifold oldManifold) 733 | { 734 | } 735 | 736 | public void PostSolve(Contact contact, ContactImpulse impulse) 737 | { 738 | } 739 | } 740 | } 741 | --------------------------------------------------------------------------------