Siedler 3 - COM Objekte, DirectDraw und Fenstermodus

Grau, teurer Freund, ist alle Theorie, und Grün des Lebens goldner Baum. (Mephisto)

Diese Thorie wird, so leid es mir auch tut, den gesammten folgenden Artikel durchziehen. Man braucht ein paar Grundlagen, auf die man später zurückgreifen kann, wenn es richtig spassig wird. Diese Grundlagen sind zu vergleichen mit einem Bauplan: Woher sollten wir wissen, an welchen Schrauben wir drehen müssen und wie diese Aussehen, um ein bestimmtes Ziel zu erreichen?

Fangen wir mit den COM-Objekten, bzw genauer mit Interfaces und ihren “Virtual Tables” (VTables) an. Ein Objekt das von einem Interface erbt, ist gezwungen, die im Interface definierten Methoden zu implementieren (die Methoden zu “überladen”). Dabei kommen sog. Funktionszeiger zum Einsatz, die auf eine Funktionsaddresse zeigen. Von dieser Funktion ist der Prototype bekannt, also wird diese Adresse angesprungen, nachdem die bekannten Argumente auf den Stack gepushed wurden. COM-Interfaces sind spezielle Interfaces, die alle von IUnknown erben. Konkret bedeutet das, dass jedes COM-Objekt die 3 in IUnknown deklarierten Methoden “QueryInterface”, “AddRef” und “Release” implementiert. QueryInterface erstellt ein Interface, das einer IID entspricht. Im Post CLRHosting Reloaded wurde z.B. ein Interface vom Typ “IID_CLRRuntimeHost” erstellt, das uns wiederrum bestimmte Funktionen bereitgestellt hat. AddRef erhöht den Referenzcount, Release erniedrigt den Referenzcount und löscht das Objekt, falls dieser Null wird. Wichtig ist hier zu erwähnen, dass die VTable eines Interfaces, wenn es erstellt und gelöscht wurde, sich noch immer an der selben Stelle im Speicher befindet. In OllyDbg sieht ein solches Interface folgendermaßen aus:

S3_Interface1

Hier haben ein direkten Call eines Interfaces. Es handelt sich dabei (wie bei allen Interface Calls) um die sog. Thiscall-Convention, heißt im Register ECX wird ein THIS_POINTER übergeben, sonst werden die anderen Argumente normal auf den Stack gepushed. Nun wird CALL [ECX+80h] aufgerufen, was einem Funktionsaufruf in der VTable entspricht. Genauer gesagt die Funktion (80h / 4), also 20h oder 32. Im Dump-Window habe ich hier ECX geladen, was direkt der VTable entspricht. Die ersten 3 markierten Funktionen Funktionen ensprechen den Funktionen aus IUnknown (QueryInterface, AddRef, Release), in Orange ist die aufgerufene Funktion “Unlock” markiert. Schauen wir doch mal in die ddraw.h, in welcher alle unsere Interfaces deklariert sind.

/*
 * IDirectDrawSurface3 and related interfaces
 */
#undef INTERFACE
#define INTERFACE IDirectDrawSurface3
DECLARE_INTERFACE_( IDirectDrawSurface3, IUnknown )
{
    /*** IUnknown methods ***/
    STDMETHOD(QueryInterface) (THIS_ REFIID riid, LPVOID FAR * ppvObj) PURE;
    STDMETHOD_(ULONG,AddRef) (THIS)  PURE;
    STDMETHOD_(ULONG,Release) (THIS) PURE;
    /*** IDirectDrawSurface methods ***/
    STDMETHOD(AddAttachedSurface)(THIS_ LPDIRECTDRAWSURFACE3) PURE;
    STDMETHOD(AddOverlayDirtyRect)(THIS_ LPRECT) PURE;
    STDMETHOD(Blt)(THIS_ LPRECT,LPDIRECTDRAWSURFACE3, LPRECT,DWORD, LPDDBLTFX) PURE;
    STDMETHOD(BltBatch)(THIS_ LPDDBLTBATCH, DWORD, DWORD ) PURE;
    STDMETHOD(BltFast)(THIS_ DWORD,DWORD,LPDIRECTDRAWSURFACE3, LPRECT,DWORD) PURE;
    STDMETHOD(DeleteAttachedSurface)(THIS_ DWORD,LPDIRECTDRAWSURFACE3) PURE;
    STDMETHOD(EnumAttachedSurfaces)(THIS_ LPVOID,LPDDENUMSURFACESCALLBACK) PURE;
    STDMETHOD(EnumOverlayZOrders)(THIS_ DWORD,LPVOID,LPDDENUMSURFACESCALLBACK) PURE;
    STDMETHOD(Flip)(THIS_ LPDIRECTDRAWSURFACE3, DWORD) PURE;
    STDMETHOD(GetAttachedSurface)(THIS_ LPDDSCAPS, LPDIRECTDRAWSURFACE3 FAR *) PURE;
    STDMETHOD(GetBltStatus)(THIS_ DWORD) PURE;
    STDMETHOD(GetCaps)(THIS_ LPDDSCAPS) PURE;
    STDMETHOD(GetClipper)(THIS_ LPDIRECTDRAWCLIPPER FAR*) PURE;
    STDMETHOD(GetColorKey)(THIS_ DWORD, LPDDCOLORKEY) PURE;
    STDMETHOD(GetDC)(THIS_ HDC FAR *) PURE;
    STDMETHOD(GetFlipStatus)(THIS_ DWORD) PURE;
    STDMETHOD(GetOverlayPosition)(THIS_ LPLONG, LPLONG ) PURE;
    STDMETHOD(GetPalette)(THIS_ LPDIRECTDRAWPALETTE FAR*) PURE;
    STDMETHOD(GetPixelFormat)(THIS_ LPDDPIXELFORMAT) PURE;
    STDMETHOD(GetSurfaceDesc)(THIS_ LPDDSURFACEDESC) PURE;
    STDMETHOD(Initialize)(THIS_ LPDIRECTDRAW, LPDDSURFACEDESC) PURE;
    STDMETHOD(IsLost)(THIS) PURE;
    STDMETHOD(Lock)(THIS_ LPRECT,LPDDSURFACEDESC,DWORD,HANDLE) PURE;
    STDMETHOD(ReleaseDC)(THIS_ HDC) PURE;
    STDMETHOD(Restore)(THIS) PURE;
    STDMETHOD(SetClipper)(THIS_ LPDIRECTDRAWCLIPPER) PURE;
    STDMETHOD(SetColorKey)(THIS_ DWORD, LPDDCOLORKEY) PURE;
    STDMETHOD(SetOverlayPosition)(THIS_ LONG, LONG ) PURE;
    STDMETHOD(SetPalette)(THIS_ LPDIRECTDRAWPALETTE) PURE;
    STDMETHOD(Unlock)(THIS_ LPVOID) PURE;
    STDMETHOD(UpdateOverlay)(THIS_ LPRECT, LPDIRECTDRAWSURFACE3,LPRECT,DWORD, LPDDOVERLAYFX) PURE;
    STDMETHOD(UpdateOverlayDisplay)(THIS_ DWORD) PURE;
    STDMETHOD(UpdateOverlayZOrder)(THIS_ DWORD, LPDIRECTDRAWSURFACE3) PURE;
    /*** Added in the v2 interface ***/
    STDMETHOD(GetDDInterface)(THIS_ LPVOID FAR *) PURE;
    STDMETHOD(PageLock)(THIS_ DWORD) PURE;
    STDMETHOD(PageUnlock)(THIS_ DWORD) PURE;
    /*** Added in the V3 interface ***/
    STDMETHOD(SetSurfaceDesc)(THIS_ LPDDSURFACEDESC, DWORD) PURE;
};

Hier finden wir, welch Überraschung, an Stelle 33 (VTable fängt bei 0 an) die Funktion “Unlock”. Nun wissen wir, wo wir in Zukunft “unbekannte” Offsets in der VTable nachschlagen können. Aber wir wissen auch, wenn wir ein solches Interface erstellt haben, wo unsere VTable liegt! Und das können wir ausnutzen!

Natürlich könnten wir nur die Funktionen hooken, die uns interessieren. So wird es z.B. bei den meisten DirectX9 Hacks gemacht: diese holen sich die VTable und hooken den EndScene Offset, also die Funktion, die vom Spiel vor dem kompletten Zeichnen der Szene gecalled wird. Da wir aber größere Modifikationen vorhaben, erstellen wir ein Wrapper für all diese Funktion. In Klartext bedeutet dies, dass wir alle Funktionen aus der VTable zu unserem Wrapper umleiten und der Wrapper die orginale Funktion called. So haben wir vollen Zugriff, wie, wann und mit welchen Parametern eine Funktion aus dem Interface aufgerufen wird. Und wir können auch bestimmen, ob wir die Funktion nicht einfach überspringen wollen ;)

Wie der eigentliche Wrapper aussieht, würde den Umfang des Artikels sprengen. Aber im Prinzip erstellen wir gleich zu beginn des Programms ein eigenes DirectDrawDevice / DirectDrawSurface und erhalten so die VTable. Nach hooken aller Funktionen löschen wir unser erstelltes Device/Surface wieder. Jeder Interface-Call wird nun auf unseren Wrapper weitergeleitet, egal wann, wie und wo diese Objekte erzeugt werden. In Normalfall wird jeder Funktionsaufruf (samt Parameter) ins Logfile geschrieben:

19:25:49 IDirectDraw2(001ED110)::Release( 0 )
19:25:49 IDirectDraw2(001ED130)::GetDisplayMode()
19:25:49 IDirectDraw2(001ED130)::SetCooperativeLevel( hWnd = 0x1600a6, dwFlags = DDSCL_ALLOWREBOOT | DDSCL_NORMAL ) = 0x0

Damit hätten wir die COM-Interfaces abgehakt, also weiter zum Fenstermodus! Siedler 3 hat keinen Fenstermodus implementiert, daher müssen wir uns behelfen. Meine ersten, zugebenenermaßen naiven Versuche waren, einfach CreateWindowEx zu hooken und dort die Fenstergröße nach meinen Ansprüchen zu setzen. DirectX machte mir aber beim erstellen des Surfaces (= die Oberfläche, worauf gezeichnet wird) einen Strich durch die Rechnung: Alles wurde im Vollbild mit Vollbildmaßen erstellt! Ein korriegieren von dieser Intialisierung sorgte dafür, dass mir beim Zeichnen INVALID_RECT Meldungen um die Ohren flogen.

Der Hintergrund zum Festermodus ist dieser: Es wird nebem dem primären Surface ein Backbuffer erzeugt, der im Hintergrund beschrieben wird und immer mit dem primären Surface ersetzt wird. Im Vollbildmodus werden diese nur umgedreht, im Fenstermodus muss man einen speziellen Zeichenvorgang aufrufen. Des weiteren muss im Fenstermodus das primäre Surface per “Clipper” an das Fenster gebunden werden. Auf jeden Fall alles Probleme, die einen Laien wie mich zum verzweífeln bringen. Abhilfe schafft das Programm DxWnd, das so ziemlich jedes DirectX Programm im Fenstermodus starten lässt. Dabei wrapped es ebenfalls alle Interface Calls und emuliert die entsprechenden Buffer und den Clipper. Vieeeel schwarze Magie :D

Doch ein Programm, das nicht darauf ausgelegt ist, im Fenstermodus zu laufen, macht das auch nicht so einfach. So errechnet sich oft der Mausoffset falsch oder die Grafiken werden verzogen gezeichnet. Beides ist bei Siedler 3 zum Glück nicht der Fall, dafür macht das DirectInput Device zicken. Siedler 3 ist es gewöhnt, das DirectInput Device ständig “acquiren” zu können, also direkten Zugriff zu erwerben. Dies geht nicht, wenn die Applikation im Hintergrund ist (Dann hat das Device keine “Priorität”, es kommt zu einer nicht gefangen Exception -> Absturz). Dies wurde umgangen, indem ich im gewrappten DirectInputDevice beim erzeugen, keine “Exklusiven” Rechte gesetzt habe, so dass es auch im Hintergrund arbeiten kann. Zudem prüfe ich, ob sich das Siedler3 Fenster im Vordergrund befindet.

Mit diesen Vorraussetzungen haben wir nun ein debugbares Spiel geschaffen, das uns nicht um die Ohren fliegt wenn der Debugger anspringt oder die Applikation in den Hintergrund verschoben wird. Nun beginnt der spassige Teil, aber erst im nächsten Artikel!

Greez

11 people like this post.
    • Philipp
    • 12. Mrz. 2014 9:41am

    Tolle Arbeit!
    Könntest du die modifizierte EXE veröffentlichen oder mir zukommen lassen?
    Es wäre schön S3 im Fenstermodus spielen zu können.

      • Easysurfer
      • 12. Mrz. 2014 11:01am

      Die modifizierte Exe wirds erst bei dem ganzen Release veröffentlicht. Bis dahin kannst Du DxWnd verwenden, das klappt soweit auch gut. Nur oftmals speichern, da es ab und an zu Abstürzen kommt .

  1. Noch keine TrackBacks.