Skip to content

Commit c935fc7

Browse files
author
sourcehold
committed
docs: include 'workings' page to discuss the workings of the reimplementation
1 parent 10ee598 commit c935fc7

File tree

2 files changed

+154
-1
lines changed

2 files changed

+154
-1
lines changed

docs/wiki.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ Wiki
44

55
Welcome to the OpenSHC Wiki!
66

7-
Here you'll find detailed documentation about:
7+
Here you'll find detailed documentation about the Project and the Game
88

9+
The OpenSHC Project
10+
-------------
11+
- :doc:`How reimplementation works <wiki/wiki-workings>`
12+
13+
The Game itself
14+
-----------
915
- :doc:`Load balancing of the core game engine <wiki/load-balancing-table>`
1016
- Game Mechanics
1117
- AI Behavior

docs/wiki/wiki-workings.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# How reimplementation works
2+
The reimplementation currently works by creating a windows DLL containing reimplemented functions.
3+
4+
## Detouring and hooking original functions to check reimplementation quality
5+
The project progresses by incrementally including new reimplemented functions. When a new function is reimplemented, its workings can be compared on the machine-code level (`reccmp`) and/or on the functional level (does the game run the same with and without the reimplementation).
6+
7+
The game's functions are hooked and detoured into the DLL's reimplemented functions. An added benefit to this approach is that Debug Symbols improve the developer experience.
8+
9+
Hooking and detouring of course comes with a performance hit but for reimplementation purposes this will likely be negligible. A full reimplementation can reduce the need for hooks.
10+
11+
## Interfacing with the game
12+
It is essential that data structures and function calls look the same as the original game.
13+
14+
Most the game's classes are known: it is clear which function belongs to which class.
15+
16+
The current approach consists of a directory and namespace layout that mimicks the roles of the game's classes.
17+
18+
Note that many classes follow the Singleton pattern. Other classes, such as Scenario Events, do not have any virtual functions. The main exception to this is C++ code such as strings and pipes (`<<`), but there original C++ definition is well-known and understood.
19+
20+
These facts simplify the approach to interfacing with the game.
21+
22+
### The use of macros
23+
Macros are used to aid in interfacing with the game. They are needed because C++ doesn't really like the use of class methods based on function pointers, and we rely on function pointers to interface with the original game.
24+
25+
Some crucial macros are those to invoke an original class method that has not been reimplemented yet.
26+
27+
### Reimplementing Classes
28+
A reimplemented class can look as follows. A class is defined that extends the struct part of the original class.
29+
Preferably this struct is defined in a different file. For example, `validTiles` is defined there as a `unsigned char[400][400]` (perhaps should be a `bool`).
30+
31+
The function `isvalidXY` has a reimplementation that can be toggled on or off in the compilation using macro define statements.
32+
33+
The original function of the game is called via the macro `MemberFunctionPointer_InvokeMethod`. The reason for using a macro is that the C++ hack that is needed here is error-prone if typed manually.
34+
35+
```cpp
36+
#pragma once
37+
38+
#include "../game/constants.h"
39+
#include "../game/common.h"
40+
#include "../game/structs/ViewportRenderState.h"
41+
42+
namespace ViewportRenderState {
43+
class ViewportRenderStateClass : ViewportRenderStateStruct
44+
{
45+
public:
46+
47+
48+
// The one that is called from outside
49+
int isValidXY(int x, int y) {
50+
#if !defined(REIMPLEMENT_IF_AVAILABLE) && !defined(REIMPLEMENT_ViewportRendererState_isValidXY) && !defined(REIMPLEMENT_0x401000)
51+
// No reimplementation defined, so call the original
52+
return this->_isValidXY(x, y);
53+
#else
54+
55+
// The reimplementation. In this case, this reimplementation produces the exact same machine code.
56+
return (x < MAP_XY_LIMIT) && (y < MAP_XY_LIMIT) && this->validTiles[y * MAP_XY_LIMIT + x] == 1;
57+
58+
#endif
59+
}
60+
61+
// The invocation to the original nicely wrapped
62+
int _isValidXY(int x, int y) {
63+
// This calls the original isValidXY logic
64+
// This is optimized to a jmp in x86! (/O2)
65+
return MemberFunctionPointer_InvokeMethod(_ViewportRenderStateClass,
66+
_isValidXY, x, y
67+
);
68+
}
69+
70+
int translateXYToTile(int x, int y);
71+
};
72+
}
73+
```
74+
75+
#### Shim classes
76+
For each class there is also a "shim" class used to trick the C++ compiler into executing method function pointers (i.e. to make the macro work). It contains only virtual functions without any implementations.
77+
```cpp
78+
#pragma once
79+
namespace ViewportRenderState {
80+
81+
// Class containing the virtual functions so we can access
82+
// the original game's class member functions
83+
class _ViewportRenderStateClass {
84+
public:
85+
86+
// The original game code's version of this function
87+
// Every virtual function must not be directly called
88+
virtual int _isValidXY(int x, int y);
89+
90+
virtual int _translateXYToTile(int x, int y);
91+
};
92+
}
93+
```
94+
95+
#### Macros for singletons and method pointers
96+
Then, to be able to use the original game code, functions pointers and a singleton pointer to the class are declared and defined. Macros take care of this part:
97+
```cpp
98+
#pragma once
99+
100+
101+
namespace ViewportRenderState {
102+
103+
104+
/*
105+
Singleton pointer
106+
*/
107+
MemberFunctionPointer_DefineSingleton(ViewportRenderStateClass, ADDRESS_ViewportRendererState_Singleton);
108+
109+
/*
110+
isValidXY
111+
*/
112+
MemberFunctionPointer_DefineProcType(ViewportRenderStateClass, _isValidXY, int, int x, int y);
113+
MemberFunctionPointer_DefineMethodType(ViewportRenderStateClass, _isValidXY, int, int x, int y);
114+
MemberFunctionPointer_DefineMethodCast(ViewportRenderStateClass, _isValidXY);
115+
MemberFunctionPointer_DefineThiscallType(ViewportRenderStateClass, _isValidXY, int, int x, int y);
116+
MemberFunctionPointer_DefineThiscall(ViewportRenderStateClass, _isValidXY, int, int x, int y);
117+
118+
MemberFunctionPointer_DefineProcType(_ViewportRenderStateClass, _isValidXY, int, int x, int y);
119+
MemberFunctionPointer_DefineMethodType(_ViewportRenderStateClass, _isValidXY, int, int x, int y);
120+
MemberFunctionPointer_DefineMethodCast(_ViewportRenderStateClass, _isValidXY);
121+
MemberFunctionPointer_DefineMethodPointer(_ViewportRenderStateClass, _isValidXY, ADDRESS_ViewportRendererState_isValidXY);
122+
123+
124+
/*
125+
translateXYToTile
126+
*/
127+
MemberFunctionPointer_DefineProcType(ViewportRenderStateClass, _translateXYToTile, int, int x, int y);
128+
MemberFunctionPointer_DefineMethodType(ViewportRenderStateClass, _translateXYToTile, int, int x, int y);
129+
MemberFunctionPointer_DefineMethodCast(ViewportRenderStateClass, _translateXYToTile);
130+
MemberFunctionPointer_DefineThiscallType(ViewportRenderStateClass, _translateXYToTile, int, int x, int y);
131+
MemberFunctionPointer_DefineThiscall(ViewportRenderStateClass, _translateXYToTile, int, int x, int y);
132+
133+
MemberFunctionPointer_DefineProcType(_ViewportRenderStateClass, _translateXYToTile, int, int x, int y);
134+
MemberFunctionPointer_DefineMethodType(_ViewportRenderStateClass, _translateXYToTile, int, int x, int y);
135+
MemberFunctionPointer_DefineMethodCast(_ViewportRenderStateClass, _translateXYToTile);
136+
MemberFunctionPointer_DefineMethodPointer(_ViewportRenderStateClass, _translateXYToTile, ADDRESS_ViewportRendererState_translateXYToTile);
137+
}
138+
```
139+
140+
## Unfinished concepts and to-do's
141+
### High priority
142+
#### Detours and hooks
143+
To permit easier reimplementation in a gradual way, individual methods should be detourable from the original game to the new definition. This can be achieved by either:
144+
1. patching the call to the original method so it goes to the reimplementation, or
145+
2. by hooking the original method so it detours to the reimplemented method.
146+
147+
The latter needs some thought to avoid infinite loops. Basically, `_isValidXY` would not optimize to a jmp because the location of the detour trampoline is only known at runtime, not at compile time. Thus `MemberFunctionPointer_DefineMethodPointer` needs a variation to deal with this case (which shouldn't be too hard...).

0 commit comments

Comments
 (0)