Entwickler-Ecke

Windows API - Keyboard-Tastendrücke aufnehmen / simulieren - Zeitkritisch


Limster - Mo 05.01.09 21:43
Titel: Keyboard-Tastendrücke aufnehmen / simulieren - Zeitkritisch
Hallo!

Ich habe ein Spiel (nicht meines) , wobei ein Level praktischerweise durch drücken einer Taste beginnt,
daher sollte es sich erst recht für mein Vorhaben eignen.

Ich möchte die ersten paar Minuten nicht immer selbst bis zu einer bestimmten Stelle spielen,
daher möchte ich die gedrückten Tasten aufnehmen und im nachhinein simulieren.

Jedoch kriege ich es nicht hin, dass es zeitlich mit meinen eigenen Eingaben exakt übereinstimmt.
Nach ein paar Sekunden schon passt was nicht und er lenkt zu früh ein.
Das Spiel beginnt aber 100% immer exakt an der gleichen Stelle und wird mit der "VK_UP" - Taste gestartet

Habe schon verschiedenes probiert, mit einem Tastatur-Hook oder alle 1ms die Keystate abfragen,
simulieren mittels SendInput oder keybd_event. Nix hilft, obwohl ich natürlich Timestamps verwende.

Hier mein code, nicht schön, da in kurzer Zeit zusammengeschustert (nur zum testen) :
- es ist die die Version OHNE Hook .. dieses zeitliche Problem habe ich mit beiden Versionen
- das Spiel benutzt wohl directInput, darum wandle ich VK_LEFT usw nochmal um


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
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:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
unit FMain;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, LTypes;

type
  TKeyStateInfo = class
    TimeStamp : TDateTime;
    VKey      : Integer;
    State     : Integer;
  end;

  TForm1 = class(TForm)
    tiSend: TTimer;
    Button1: TButton;
    meInput: TMemo;
    nbListen: TButton;
    nbClear: TButton;
    tiStartSend: TTimer;
    tiReadKeyStates: TTimer;
    procedure Button1Click(Sender: TObject);
    procedure tiSendTimer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure nbListenClick(Sender: TObject);
    procedure nbClearClick(Sender: TObject);
    procedure tiStartSendTimer(Sender: TObject);
    procedure tiReadKeyStatesTimer(Sender: TObject);
  private
    FCatchBeginTime: TDatetime;
    FSendBegintime : TDateTime;
    FKeyStates     : TStringList;
    fCount: Integer;
    fListen : Boolean;
    FKeyLog   : TList;
    FKeyLogTmp: TList;
    { Private declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  Math;

const
  RecordKeys : Array [0..4of Integer = (VK_LEFT,
                                          VK_RIGHT,
                                          VK_UP,
                                          VK_DOWN,
                                          VK_LCONTROL);

{$R *.dfm}
/// SENDEN DER AUFGENOMMENEN KEYS STARTET IN 5 Sekunden
procedure TForm1.Button1Click(Sender: TObject);   
begin
  FKeyLogTmp.Assign(FKeyLog);
  tiStartSend.Enabled := not tiSend.Enabled;
  if tiStartSend.Enabled then begin
    Self.Caption := 'EIN';
  end else begin
    tiSend.Enabled := FALSE;
    Self.Caption := 'AUS';
  end;
end;

// WIRD ALLE 1 MS AUSGEFÜHRT
procedure TForm1.tiSendTimer(Sender: TObject);
var
  lObj   : TKeyStateInfo;
  lInput : TInput;
  lDxKey : Integer;
begin
  if Assigned(FKeyLogTmp) then begin
    while (FKeyLogTmp.Count > 0do begin
      lObj := TKeyStateInfo(FKeyLogTmp.Items[0]);
      if (lObj.TimeStamp <= (now - FSendBegintime)) then begin
        lDxKey := lObj.VKey;
        case lObj.VKey of
          VK_LEFT      : lDxKey := 203;
          VK_RIGHT     : lDxKey := 205;
          VK_UP        : lDxKey := 200;
          VK_DOWN      : lDxKey := 208;
          VK_LCONTROL  : lDxKey := 29;
        end;

        lInput.Itype    := INPUT_KEYBOARD;
        lInput.ki.wVk   := 0;
        lInput.ki.wScan := lDxKey;
        lInput.ki.time  := 0;
        lInput.ki.dwExtraInfo := 0;
        if (lObj.State < 0then begin
          lInput.ki.dwFlags := 0;
        end else begin
          lInput.ki.dwFlags := KEYEVENTF_KEYUP;
        end;
        SendInput(1, lInput, SizeOf(lInput));

        FKeyLogTmp.Delete(0);
      end else begin
        break;
      end;
    end;
  end;   
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FKeyLog := TList.Create;
  FKeyLogTmp := TList.Create;
  FKeyStates := TStringList.Create;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FKeyStates.Free;
  FKeyLog.Free;  // TODO: Obj freigeben
  FKeyLogTmp.Free;
end;

//STARTET / STOPPT DAS AUFNEHMEN DER KEYS
procedure TForm1.nbListenClick(Sender: TObject);
begin
  tiReadKeyStates.Enabled := not tiReadKeyStates.Enabled;
  if tiReadKeyStates.Enabled then begin
    FCatchBeginTime  := now;
    nbListen.Caption := 'Listen - ON';
  end else begin
    nbListen.Caption := 'Listen - OFF';
  end;
end;

procedure TForm1.nbClearClick(Sender: TObject);
begin
  meInput.Lines.Clear;
  FKeyStates.Clear;
  FKeyLog.Clear;
end;

procedure TForm1.tiStartSendTimer(Sender: TObject);
begin
  FSendBegintime := now;
  tiSend.Enabled := TRUE;
  tiStartSend.Enabled := FALSE;
end;

//WIRD ALLE 1 MS AUSGEFÜHRT
procedure TForm1.tiReadKeyStatesTimer(Sender: TObject);
var
  i      : Integer;
  lObj   : TKeyStateInfo;
  lState : Integer;
begin
  for i := Low(RecordKeys) to High(RecordKeys) do begin
    lState := GetAsyncKeyState(RecordKeys[i]);
    if (lState < 0then lState := -1
    else                 lState := 1;
    if (FKeyStates.Values[IntToStr(RecordKeys[i])] <> InttoStr(lState)) then begin
      FKeyStates.Values[IntToStr(RecordKeys[i])] := IntToStr(lState);
      lObj := TKeyStateInfo.Create;
      lObj.TimeStamp := now - FCatchBeginTime;
      lObj.VKey      := RecordKeys[i];
      lObj.State     := lState;
      meInput.Lines.Add('Key: ' + IntToStr(RecordKeys[i]));
      FKeyLog.Add(lObj);
    end;
  end;
end;

end.



Ist mein Anliegen überhaupt lösbar ? Oder ist das ganze Tastatursimulieren niemals so genau ?

mfg,
Limster


GTA-Place - Di 06.01.09 01:59

Das mit dem Timer wird nicht funktionieren. Es ist unmöglich, dass der Timer jede Millisekunde aufgerufen wird (minimum (gerade getestet) 15ms). Außerdem ist der Timer sehr sehr ungenau. Wenns richtig schnell gehen soll, dann ist OnIdle das richtige (< 1/100 ms). Aber normalerweise muss es ja gar nicht so genau sein.


toms - Di 06.01.09 06:53

Hallo

Hast du es mit einem WH_ JOURNALRECORD Hook probiert?


Limster - Di 06.01.09 15:48
Titel: DirectX
Danke für den Tipp..

Hab das jetzt mal ausprobiert. funktioniert so im allgemeinen supa.
Menüführung im Spiel automatisieren funktioniert wunderbar,
jedoch wenns dann zum Spielen wird, reagiert mein Spieler nicht..

Wird ein DirectX / DirectInput Problem sein ?

Warum muss das so schwierig sein ;)

mfg


Boldar - Di 06.01.09 15:51

mmh villeicht soll genau das im Spiel verhindert werden...
du könntest dir alternativ auch ein G-15 Keyboard kaufen, da is das von Haus aus mit dabei...


Limster - Di 06.01.09 16:08

nein das denke ich nicht.
Mit meinen anderen Versuchen (normalem Keyboard hook, bzw. Timer) habe ich zb. zwar
VK_LEFT aufgenommen, gesendet (SendInput oder Keyb_Event) habe ich jedoch 203 (habe ich mir gegoogelt -> 203 ist für DirectInput ein Links) als Scancode und nicht
VK_LEFT.. dann hat sich auch der Spieler bewegt .. aber hier war halt das "zeitliche" Problem..

mfg


jaenicke - Di 06.01.09 16:30

Wie hast du es denn jetzt gemacht?

Kannst du da nicht auch die Keycodes entsprechend ändern?


Limster - Di 06.01.09 17:28

das krieg ich grad nicht gebacken:

The paramL member of the EVENTMSG structure specifies the virtual key code of the key that was pressed.
The paramH member of the EVENTMSG structure specifies the scan code.


es scheint aber so, als ob das nicht so daherkommt wie beschrieben..

habs getestet wenn ich die "Rechts"- Taste drücke:

paramL ist nicht VK_RIGHT (=38) .. sondern 19751
paramH dürfte auch kein Scancode sein (=32845) .. zumindest liefert MapVirtualKey immer 0

--> MapVirtualKey: für die MapType-Übergabe find ich keine Konstante, habs aber mit 0, 1 bzw 2 ausprobert um vom
ScanCode auf den VK zu kommen

hm..

mfg


Limster - Di 06.01.09 18:01

Hab das EventMsg doch gebacken bekommen.. mehr oder weniger, der Spieler rührt sich aber so nicht :

bei meinen anderen Versionen, bei denen sich wenigstens der Spieler bewegt hat,
hatte ich für das Keysimulieren als VKCode = 0 und als ScanCode zb. 203 geschickt.

Hier habe ich es so gelöst: (funktioniert aber nicht)

Low-Order Byte von LParam is der VKKeyCode, High-Order Byte von LParam wäre der Scancode


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
        if (lEventStrut.message = WM_KEYDOWN) or (lEventStrut.message = WM_KEYUP) then begin
          case Lo(lEventStrut.paramL) of
            VK_LEFT : begin
              lEventStrut.paramL := (203 shl 8);
            end;
            VK_UP: begin
              lEventStrut.paramL := (200 shl 8);
            end;
            VK_RIGHT: begin
              lEventStrut.paramL := (205 shl 8);
            end;
            VK_DOWN: begin
              lEventStrut.paramL := (208 shl 8);
            end;
          end;
        end;