65.9K
CodeProject 正在变化。 阅读更多。
Home

FreeCell Discombobulator

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (9投票s)

2005年10月5日

CPOL

3分钟阅读

viewsIcon

62648

downloadIcon

1530

掌控 FreeCell

注意:这个应用程序只在 Win 2K 和 Win XP 上有效!

引言

空当接龙是 Windows 自带的游戏之一。这个游戏最棒的地方在于它总是可以赢的 - 不像其他纸牌游戏 - 在我看来这使它成为最好的游戏。但是,如果你行动太快,你很容易陷入一个不可赢的位置。这个应用程序允许你随意移动卡片来克服这种状态。

但要警告一下 - 如果你经常“作弊”,它会破坏游戏的乐趣。

背景

我偶然发现了一篇名为 FreeCell & Hearts, Behind the scenes [^] 的文章,作者是 arikp [^],他的侦探工作使我能够编写一个调整器。我没有使用他的任何代码,只使用了他发现的内存地址和结构。他只找到了 Win 2K 和 Win XP 的信息,这就是为什么这个应用程序只在这些系统上有效。

使用应用程序

运行时,此应用程序会附加到当前正在运行的第一个空当接龙实例。它会重现卡片数组,并允许你只需将卡片拖放到你想要的任何位置。

它还会重现“游戏”菜单,但这些菜单的功能与空当接龙略有不同:“新游戏”、“选择游戏”和“重新开始游戏”选项都会在执行其功能之前赢得你当前的游戏;而“统计信息”选项允许你编辑空当接龙显示的值。“刷新”选项只会从空当接龙更新状态,但由于应用程序在激活时会自动刷新,因此不需要此选项。

进程”菜单提供了两个选项:“终止”和“重启”。这两个选项都会以极端的方式终止当前的空当接龙进程,但“重启”选项会启动一个新的空当接龙实例。如果你只是对游戏感到厌烦,这些选项很有用。

还有一个“彩蛋”功能:按 F12,调整器将按等级和花色排列剩余的卡片;只需双击一下,你就赢得了游戏!

代码

Arik 的文章揭示了空当接龙内部使用的内存地址和结构。这个应用程序使用这些信息来读取和写入空当接龙进程的虚拟内存。它通过使用 Kernel32 中的 OpenProcessReadProcessMemoryWriteProcessMemory 函数来实现这一点。

这个应用程序还通过模拟键盘和鼠标事件来控制空当接龙进程。我使用 SendKeys .NET 类来模拟键盘事件,以及 User32 中的 SendInput Win32 函数来模拟鼠标事件。

唯一要提及的另一件事是,我使用 Cards.dll 来绘制卡片,这让生活变得轻松。

关注点

进程内存访问

我使用以下声明来访问 Kernel32 中的函数

[ DllImport( "Kernel32.dll", SetLastError = true ) ]
public static extern IntPtr OpenProcess
(
    UInt32 dwDesiredAccess,
    bool bInheritHandle,
    UInt32 dwProcessId
);

[ DllImport( "Kernel32.dll" ) ]
public static extern bool ReadProcessMemory
(
    IntPtr hProcess,
    IntPtr lpBaseAddress,
    byte[] lpBuffer,
    UInt32 nSize,
    ref UInt32 lpNumberOfBytesRead
);

[ DllImport( "Kernel32.dll" ) ]
public static extern Int32 WriteProcessMemory
(
    IntPtr hProcess,
    IntPtr lpBaseAddress,
    byte[] buffer,
    UInt32 size,
    ref UInt32 lpNumberOfBytesWritten
);

SendKeys

这个 .NET 类将键发送到前台进程,所以我使用 User32 中的 SetForegroundWindow 将空当接龙置于前台,然后再发送所需的键

[ DllImport( "User32.dll" ) ]
public static extern bool SetForegroundWindow
(
    IntPtr hWnd
);

SendInput

这有点棘手。我使用了以下 PInvoke 声明

internal class INPUT
{
    public const int MOUSE = 0;
    public const int KEYBOARD = 1;
    public const int HARDWARE = 2;
}

internal class MOUSEEVENTF
{
    public const int MOVE = 0x0001 ;        /* mouse move */
    public const int LEFTDOWN = 0x0002 ;    /* left button down */
    public const int LEFTUP = 0x0004 ;      /* left button up */
    public const int RIGHTDOWN = 0x0008 ;   /* right button down */
    public const int RIGHTUP = 0x0010 ;     /* right button up */
    public const int MIDDLEDOWN = 0x0020 ;  /* middle button down */
    public const int MIDDLEUP = 0x0040 ;    /* middle button up */
    public const int XDOWN = 0x0080 ;       /* x button down */
    public const int XUP = 0x0100 ;         /* x button down */
    public const int WHEEL = 0x0800 ;       /* wheel button rolled */
    public const int VIRTUALDESK = 0x4000 ; /* map to entire 
                                               virtual desktop */
    public const int ABSOLUTE = 0x8000 ;    /* absolute move */
}

[ StructLayout( LayoutKind.Sequential ) ]
internal struct MOUSEINPUT 
{
    public int dx;
    public int dy;
    public int mouseData;
    public int dwFlags;
    public int time;
    public IntPtr dwExtraInfo;
}

[ StructLayout( LayoutKind.Explicit ) ]
internal struct Input 
{
    [ FieldOffset( 0 ) ] public int type;
    [ FieldOffset( 4 ) ] public MOUSEINPUT mi;
    [ FieldOffset( 4 ) ] public KEYBDINPUT ki;
    [ FieldOffset( 4 ) ] public HARDWAREINPUT hi;
}

[ DllImport( "User32.dll" ) ]
public static extern UInt32 SendInput
(
    UInt32 nInputs,
    Input[] pInputs,
    Int32 cbSize
);

MOUSEINPUT struct 使用了一种真正奇怪的坐标系:屏幕被映射到两个轴上从 0 到 65535 的比例。所以我使用以下代码从像素转换为所需的值

Rectangle screen = Screen.PrimaryScreen.Bounds;
int x2 = ( 65535 * x ) / screen.Width;
int y2 = ( 65535 * y ) / screen.Height;

然后我使用这些值来创建一个 Input structs 的数组来模拟双击

Input[] input = new Input[ 4 ];

for ( int i = 0 ; i < input.Length ; i++ )
{
    input[ i ].type = INPUT.MOUSE;
    input[ i ].mi.dx = x2;
    input[ i ].mi.dy = y2;
    input[ i ].mi.dwFlags = 
              MOUSEEVENTF.MOVE | MOUSEEVENTF.ABSOLUTE;

    if ( ( i % 2 ) == 0 )
        input[ i ].mi.dwFlags |= MOUSEEVENTF.LEFTDOWN;
    else
        input[ i ].mi.dwFlags |= MOUSEEVENTF.LEFTUP;
}

然后调用 SendInput 函数就很简单了

User32.SendInput( ( UInt32 ) input.Length, input, 
                         Marshal.SizeOf( input[0]));

双缓冲

你需要设置三种样式来双缓冲一个 Control (一个 Form 派生自 Control

public Form1()
{
    SetStyle( ControlStyles.UserPaint            , true );
    SetStyle( ControlStyles.AllPaintingInWmPaint , true );
    SetStyle( ControlStyles.DoubleBuffer         , true );

    InitializeComponent();
}

结论

编写这个小应用程序非常有趣。事实证明,控制另一个进程并以原始作者意想不到的方式扩展它是比我想象的要容易。

历史

  • 2005 年 10 月 5 日:版本 1
© . All rights reserved.