755 lines
39 KiB
C++
755 lines
39 KiB
C++
// Photo mode ([Photo] section of DS2Widescreen.ini), Dungeon Siege II (Steam v2.3) and Broken World.
|
|
// See docs/architecture/photo-mode.md and docs/roadmap/photo-mode.md.
|
|
//
|
|
// One key (default Insert) toggles photo mode, a second key (default Return) saves a PNG, Escape leaves it:
|
|
// - single player: the game is paused (AppModule::UserPause, the engine's own pause; if the player had paused the
|
|
// game already it stays paused afterwards). Multiplayer: nothing is paused, the world goes on;
|
|
// - the interface is hidden: UIShell::SetVisible(false) (the windows: HUD, portraits, tooltips, console text),
|
|
// UIGame::SetRadarVisible(false), and in multiplayer the other players' names above their heads (UIGame flag
|
|
// +0x4c0, tested by the interface pass; it is the same switch as the "toggle_player_labels" key);
|
|
// - the camera limits are widened through camera.cpp's shadowing (Limits): further zoom, flatter and steeper
|
|
// pitch; the field of view is the photo mode's own (Fov, adjustable with two keys). camera.cpp puts the
|
|
// game's own values back when photo mode ends, so nothing of the game's camera data is rewritten by hand;
|
|
// - left and right clicks are not passed on (a click would otherwise order the party about);
|
|
// - the picture is the back buffer at the point where the 3D world is finished and the interface not drawn yet
|
|
// (the shared hub's world-finished point, d3dhub.h: the same as the anti-aliasing of DS2Graphics, which runs first):
|
|
// no interface pixel can be in it. It is
|
|
// written as a PNG into the folder the engine takes its own screenshots to (RapiAppModule's "%shots_path%").
|
|
// The point exists in every [Display] Mode; only if the hub cannot install it there is no picture (log line).
|
|
// On leaving, the pause, the interface, the radar, the names and the camera state (the camera data as it was
|
|
// when photo mode began) are put back.
|
|
#include "../common/patch.h"
|
|
#include <d3d9.h>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
|
|
namespace widescreen::photo {
|
|
void Install(const ds2::Ini& ini);
|
|
bool Wanted();
|
|
bool Active();
|
|
void NoZoom();
|
|
bool Limits(void* cam, float* zoom, float* minPitch, float* maxPitch);
|
|
void Fov(float gameFov, float aspect, float* out);
|
|
void OnUpdate(void* cam, DWORD fovOff, DWORD limitsOff);
|
|
void WorldDone(IDirect3DDevice9* dev);
|
|
} // namespace widescreen::photo
|
|
|
|
namespace {
|
|
|
|
constexpr float kPi = 3.14159265f;
|
|
constexpr float kDeg = kPi / 180.0f;
|
|
|
|
// ---- settings ------------------------------------------------------------------------------------------
|
|
bool g_wanted = false;
|
|
int g_keyToggle = VK_INSERT, g_keyCapture = VK_RETURN, g_keyFovWider = VK_PRIOR, g_keyFovNarrower = VK_NEXT, g_keyFree = VK_DELETE;
|
|
float g_freeSpeed = 6.0f, g_freeRadius = 0.0f; // free camera: metres per second; 0 = from the map's world frustum radius
|
|
float g_zoom = 3.0f, g_minPitch = 10.0f, g_maxPitch = 89.0f;
|
|
float g_fovMin = 20.0f, g_fovMax = 110.0f, g_fovSpeed = 30.0f; // vertical degrees; degrees per second
|
|
|
|
// ---- engine entry points (exports; found by name in both exes) --------------------------------------------
|
|
using Singleton_t = void*(__cdecl*)();
|
|
using ThisBool_t = void(__fastcall*)(void* self, void* edx, int value);
|
|
using ThisRet_t = int(__fastcall*)(void* self, void* edx); // bool results are in AL only: mask with & 0xFF
|
|
|
|
using Free_t = int(__cdecl*)();
|
|
using FreeInt_t = int(__cdecl*)(int);
|
|
|
|
using GetCamera_t = void*(__fastcall*)(void* engine, void* edx);
|
|
GetCamera_t g_getCamera; // siege::SiegeEngine::GetCamera: the game's one main camera (the update hook also sees the interface's own cameras)
|
|
Singleton_t g_engineSingleton, g_appSingleton, g_shellSingleton, g_gameSingleton, g_uiCameraSingleton, g_worldStateSingleton, g_rapiSingleton;
|
|
ThisBool_t g_userPause, g_setVisible, g_setRadarVisible;
|
|
ThisRet_t g_isUserPaused, g_currentState;
|
|
Free_t g_isMultiPlayer;
|
|
|
|
FreeInt_t g_inGameInteractive;
|
|
using Convert_t = void(__cdecl*)(void* pos, const void* node); // ConvertSiegePosToNode(SiegePos&, database_guid const&): in place
|
|
using PtrThis_t = void*(__fastcall*)(void* self, void* edx);
|
|
using FloatThis_t = float(__fastcall*)(void* self, void* edx);
|
|
Convert_t g_convert;
|
|
Singleton_t g_serverSingleton, g_worldMapSingleton;
|
|
PtrThis_t g_getScreenHero, g_getPlacement, g_getPosition;
|
|
FloatThis_t g_frustumRadius;
|
|
DWORD o_camSiegePos = 0, o_tgtSiegePos = 0; // SiegeCamera: the camera's and the target's SiegePos (0x28 bytes: x y z, pad, node guid at +0x10)
|
|
DWORD o_shellVisible = 0; // UIShell: the flag UIShell::SetVisible writes
|
|
DWORD o_radar = 0; // UIGame: the radar object (radar+0 = visible)
|
|
DWORD o_labels = 0; // UIGame: show names of the other players (multiplayer), tested by the interface pass
|
|
bool g_hooked = false; // the hub's world-finished point (d3dhub.h) has been reached at least once
|
|
|
|
ds2::Ini* g_ini = nullptr;
|
|
// [PhotoDebug] switches (read at every toggle; diagnostics): SkipPause, SkipShell, SkipRadar, SkipLabels, SkipCameraRestore
|
|
bool Dbg(const char* key) { return g_ini && g_ini->Bool("PhotoDebug", key, false); }
|
|
|
|
template <class T>
|
|
T Export(const char* name) {
|
|
return reinterpret_cast<T>(GetProcAddress(GetModuleHandleA(nullptr), name));
|
|
}
|
|
|
|
bool Readable(const void* p, size_t n) {
|
|
MEMORY_BASIC_INFORMATION mbi;
|
|
if (!p || !VirtualQuery(p, &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT || (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))) return false;
|
|
return static_cast<const BYTE*>(p) + n <= static_cast<const BYTE*>(mbi.BaseAddress) + mbi.RegionSize;
|
|
}
|
|
|
|
// ---- state ---------------------------------------------------------------------------------------------
|
|
bool g_on = false;
|
|
bool g_mp = false;
|
|
bool g_pausedByUs = false, g_wasPaused = false;
|
|
bool g_shellWas = true, g_radarWas = true, g_labelsWas = false;
|
|
void* g_cam = nullptr; // SiegeCamera (from camera.cpp's update hook)
|
|
DWORD g_fovOff = 0, g_limitsOff = 0;
|
|
constexpr DWORD kCamFrom = 0x18, kUiFrom = 0x10, kUiTo = 0x200;
|
|
BYTE g_camSave[0x400], g_uiSave[0x400]; // the camera data as it was when photo mode began
|
|
void* g_uiCam = nullptr;
|
|
float g_fovV = 0; // vertical degrees in photo mode
|
|
bool g_fovInit = false;
|
|
bool g_fovWider = false, g_fovNarrower = false;
|
|
LARGE_INTEGER g_fovT{};
|
|
volatile LONG g_shotRequested = 0;
|
|
// Selection rings and the hover outline are world effects drawn before the picture point. For the picture only they are switched
|
|
// off for two frames: the rings by UIGame's own flag (+0x85, tested by the ring drawing, Steam sub_46fdd0), the outline by skipping the
|
|
// screen-space quad that fills it (Steam call site 0x6db0c9 of sub_6da860 -> sub_52bb20). Both are put back right after the picture.
|
|
bool g_hideSelection = true;
|
|
DWORD o_ringOff = 0; // UIGame: "do not draw the rings"
|
|
using OutlineQuad_t = void(__cdecl*)(int, int, int, int, int, int, int);
|
|
OutlineQuad_t g_outlineQuad = nullptr;
|
|
bool g_outlineHidden = false;
|
|
int g_shotStage = 0; // 0 idle, then frames until the picture is taken
|
|
BYTE g_ringWas = 0;
|
|
|
|
void __cdecl HookOutlineQuad(int a, int b, int c, int d, int e, int f, int g) {
|
|
if (!g_outlineHidden && g_outlineQuad) g_outlineQuad(a, b, c, d, e, f, g);
|
|
}
|
|
bool g_keyDown[256];
|
|
// free camera (K2)
|
|
bool g_free = false, g_atEdge = false, g_look = false;
|
|
BYTE g_sp[2][0x28]; // camera and target SiegePos in the node of the camera at detach
|
|
float g_yaw = 0, g_pitch = 0, g_wheel = 0, g_mouseDx = 0, g_mouseDy = 0;
|
|
LARGE_INTEGER g_freeT{};
|
|
POINT g_lookLast{};
|
|
HWND g_wnd = nullptr;
|
|
WNDPROC g_orig = nullptr;
|
|
|
|
void* Obj(Singleton_t s) { return s ? s() : nullptr; }
|
|
|
|
bool InteractiveGame() {
|
|
void* ws = Obj(g_worldStateSingleton);
|
|
return ws && g_currentState && g_inGameInteractive && (g_inGameInteractive(g_currentState(ws, nullptr)) & 0xFF) != 0;
|
|
}
|
|
|
|
BYTE* ShellFlag() {
|
|
void* s = Obj(g_shellSingleton);
|
|
return s && o_shellVisible ? static_cast<BYTE*>(s) + o_shellVisible : nullptr;
|
|
}
|
|
|
|
BYTE* RadarFlag() {
|
|
void* g = Obj(g_gameSingleton);
|
|
if (!g || !o_radar) return nullptr;
|
|
BYTE* radar = *reinterpret_cast<BYTE**>(static_cast<BYTE*>(g) + o_radar);
|
|
return Readable(radar, 4) ? radar : nullptr;
|
|
}
|
|
|
|
BYTE* LabelsFlag() {
|
|
void* g = Obj(g_gameSingleton);
|
|
return g && o_labels ? static_cast<BYTE*>(g) + o_labels : nullptr;
|
|
}
|
|
|
|
void SetUserPaused(bool pause) {
|
|
void* app = Obj(g_appSingleton);
|
|
if (app) g_userPause(app, nullptr, pause ? 1 : 0);
|
|
}
|
|
|
|
bool UserPaused() {
|
|
void* app = Obj(g_appSingleton);
|
|
return app && (g_isUserPaused(app, nullptr) & 0xFF) != 0;
|
|
}
|
|
|
|
// ---- free camera (K2) --------------------------------------------------------------------------------------
|
|
// The camera and the target are SiegePos (x y z in the local space of a node, plus the node's guid). At detach both are
|
|
// taken into the space of the camera's node (ConvertSiegePosToNode) and from then on the fly controller writes them before
|
|
// every engine camera update, which turns them into world positions and builds the culling frustum: the game's own tracking
|
|
// of the party goes on underneath and is overwritten each time. A node's local space is a translation and a turn about the
|
|
// vertical axis of the world, so the controller moves in it directly (y is up). The camera may not leave the loaded part of
|
|
// the world: it is held within a radius of the party (the map's world_frustum_radius x 0.85).
|
|
bool HeroSiegePos(BYTE* out) {
|
|
void* server = Obj(g_serverSingleton);
|
|
if (!server || !g_getScreenHero || !g_getPlacement || !g_getPosition) return false;
|
|
void* hero = g_getScreenHero(server, nullptr);
|
|
void* placement = hero ? g_getPlacement(hero, nullptr) : nullptr;
|
|
const BYTE* pos = placement ? static_cast<const BYTE*>(g_getPosition(placement, nullptr)) : nullptr;
|
|
if (!pos || !Readable(pos, 0x28)) return false;
|
|
std::memcpy(out, pos, 0x28);
|
|
return true;
|
|
}
|
|
|
|
float FreeRadius() {
|
|
if (g_freeRadius > 0) return g_freeRadius;
|
|
void* wm = Obj(g_worldMapSingleton);
|
|
float r = wm && g_frustumRadius ? g_frustumRadius(wm, nullptr) : 0.0f;
|
|
return r > 8.0f && r < 500.0f ? r * 0.85f : 30.0f;
|
|
}
|
|
|
|
void EnterFree() {
|
|
if (g_free || !g_cam || !o_camSiegePos || !o_tgtSiegePos || !g_convert) return;
|
|
BYTE* c = static_cast<BYTE*>(g_cam);
|
|
std::memcpy(g_sp[0], c + o_camSiegePos, 0x28);
|
|
std::memcpy(g_sp[1], c + o_tgtSiegePos, 0x28);
|
|
g_convert(g_sp[1], g_sp[0] + 0x10); // the target in the camera's node
|
|
const float* p = reinterpret_cast<const float*>(g_sp[0]);
|
|
const float* t = reinterpret_cast<const float*>(g_sp[1]);
|
|
float dx = t[0] - p[0], dy = t[1] - p[1], dz = t[2] - p[2], len = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (len < 1e-3f) dx = 0, dy = -0.6f, dz = 0.8f, len = 1.0f;
|
|
g_yaw = std::atan2(dx, dz);
|
|
g_pitch = std::asin(dy / len);
|
|
g_wheel = g_mouseDx = g_mouseDy = 0;
|
|
g_look = false;
|
|
g_atEdge = false;
|
|
QueryPerformanceCounter(&g_freeT);
|
|
g_free = true;
|
|
ds2::log().Write("photo: free camera on at %.2f %.2f %.2f (node space of the camera), yaw %.0f pitch %.0f deg, held within %.1f m of the party", p[0], p[1], p[2],
|
|
g_yaw / kDeg, g_pitch / kDeg, FreeRadius());
|
|
}
|
|
|
|
void LeaveFree() {
|
|
if (!g_free) return;
|
|
g_free = false;
|
|
g_look = false;
|
|
const float* p = reinterpret_cast<const float*>(g_sp[0]);
|
|
ds2::log().Write("photo: free camera off (it was at %.2f %.2f %.2f); the party camera comes back", p[0], p[1], p[2]);
|
|
}
|
|
|
|
// The main camera, before each engine update while the free camera is on.
|
|
void FreeStep(void* cam) {
|
|
LARGE_INTEGER now, freq;
|
|
QueryPerformanceCounter(&now);
|
|
QueryPerformanceFrequency(&freq);
|
|
float dt = static_cast<float>(now.QuadPart - g_freeT.QuadPart) / static_cast<float>(freq.QuadPart);
|
|
g_freeT = now;
|
|
if (dt > 0.1f) dt = 0.1f;
|
|
if (dt < 0) dt = 0;
|
|
auto key = [](int vk) { return g_keyDown[vk] ? 1.0f : 0.0f; };
|
|
g_yaw += (key(VK_RIGHT) - key(VK_LEFT)) * 1.2f * dt + g_mouseDx * 0.004f;
|
|
g_pitch += (key(VK_UP) - key(VK_DOWN)) * 0.8f * dt - g_mouseDy * 0.004f;
|
|
g_mouseDx = g_mouseDy = 0;
|
|
if (g_pitch > 1.5f) g_pitch = 1.5f;
|
|
if (g_pitch < -1.5f) g_pitch = -1.5f;
|
|
float speed = g_freeSpeed * (g_keyDown[VK_SHIFT] ? 4.0f : 1.0f) * (g_keyDown[VK_CONTROL] ? 0.25f : 1.0f);
|
|
float fwd = key('W') - key('S'), side = key('D') - key('A'), vert = key('R') - key('F');
|
|
float notch = g_wheel * 3.0f; // a wheel notch = 3 m along the view direction
|
|
g_wheel = 0;
|
|
float sy = std::sin(g_yaw), cy = std::cos(g_yaw), cp = std::cos(g_pitch), sp = std::sin(g_pitch);
|
|
float* p = reinterpret_cast<float*>(g_sp[0]);
|
|
float* t = reinterpret_cast<float*>(g_sp[1]);
|
|
float step = speed * dt;
|
|
p[0] += (sy * fwd + cy * side) * step + sy * cp * notch; // forward over the ground, right = (cos, 0, -sin)
|
|
p[2] += (cy * fwd - sy * side) * step + cy * cp * notch;
|
|
p[1] += vert * step + sp * notch;
|
|
// the loaded part of the world: within the radius of the party over the ground plane, and not far above or below it
|
|
BYTE hero[0x28];
|
|
if (HeroSiegePos(hero)) {
|
|
g_convert(hero, g_sp[0] + 0x10);
|
|
const float* h = reinterpret_cast<const float*>(hero);
|
|
float dx = p[0] - h[0], dz = p[2] - h[2], r = FreeRadius(), d = std::sqrt(dx * dx + dz * dz);
|
|
bool edge = false;
|
|
if (d > r) {
|
|
p[0] = h[0] + dx * r / d;
|
|
p[2] = h[2] + dz * r / d;
|
|
edge = true;
|
|
}
|
|
if (p[1] > h[1] + r) p[1] = h[1] + r, edge = true;
|
|
if (p[1] < h[1] - 6.0f) p[1] = h[1] - 6.0f, edge = true;
|
|
if (edge && !g_atEdge) ds2::log().Write("photo: free camera at the edge of the loaded area (%.1f m from the party): it stops here", d > r ? r : d);
|
|
g_atEdge = edge;
|
|
}
|
|
t[0] = p[0] + sy * cp * 10.0f;
|
|
t[1] = p[1] + sp * 10.0f;
|
|
t[2] = p[2] + cy * cp * 10.0f;
|
|
BYTE* c = static_cast<BYTE*>(cam);
|
|
std::memcpy(c + o_camSiegePos, g_sp[0], 0x28);
|
|
std::memcpy(c + o_tgtSiegePos, g_sp[1], 0x28);
|
|
}
|
|
|
|
void LogCamera(const char* when) {
|
|
if (!g_cam || !Readable(g_cam, 0x1f0)) return;
|
|
const float* f = static_cast<const float*>(g_cam);
|
|
const float* u = g_uiCam && Readable(g_uiCam, 0x200) ? static_cast<const float*>(g_uiCam) : nullptr;
|
|
ds2::log().Write("photo: camera %s: position %.2f %.2f %.2f, target %.2f %.2f %.2f, distance field %.2f, limits %.0f..%.0f %.1f..%.1f%s", when,
|
|
f[0x178 / 4], f[0x17C / 4], f[0x180 / 4], f[0x184 / 4], f[0x188 / 4], f[0x18C / 4], f[0x19C / 4], f[g_limitsOff / 4 + 1],
|
|
f[g_limitsOff / 4], f[g_limitsOff / 4 + 2], f[g_limitsOff / 4 + 3], u ? "" : " (no UI camera)");
|
|
if (u) ds2::log().Write("photo: UICamera %s: orientation %.3f %.3f %.3f %.3f, +1f0 %.2f", when, u[0x60 / 4], u[0x64 / 4], u[0x68 / 4], u[0x6C / 4], u[0x1F0 / 4]);
|
|
}
|
|
|
|
void Enter() {
|
|
if (g_on) return;
|
|
void* engine = Obj(g_engineSingleton);
|
|
g_cam = engine ? g_getCamera(engine, nullptr) : nullptr;
|
|
if (!g_cam || !g_limitsOff || !InteractiveGame() || !Readable(g_cam, g_limitsOff)) {
|
|
ds2::log().Write("photo: not now (%s)", InteractiveGame() ? "camera not seen yet" : "not in a game");
|
|
return;
|
|
}
|
|
g_uiCam = Obj(g_uiCameraSingleton);
|
|
if (g_uiCam && !Readable(static_cast<BYTE*>(g_uiCam) + kUiFrom, kUiTo - kUiFrom)) g_uiCam = nullptr;
|
|
LogCamera("before");
|
|
// the camera data as it is now (put back when photo mode ends)
|
|
std::memcpy(g_camSave, g_cam, g_limitsOff);
|
|
if (g_uiCam) std::memcpy(g_uiSave, g_uiCam, kUiTo);
|
|
g_mp = g_isMultiPlayer ? (g_isMultiPlayer() & 0xFF) != 0 : true; // unknown: never pause
|
|
g_wasPaused = UserPaused();
|
|
g_pausedByUs = false;
|
|
if (!g_mp && !g_wasPaused && !Dbg("SkipPause")) {
|
|
SetUserPaused(true);
|
|
g_pausedByUs = UserPaused();
|
|
}
|
|
if (BYTE* f = ShellFlag()) g_shellWas = *f != 0;
|
|
if (g_setVisible && Obj(g_shellSingleton) && !Dbg("SkipShell")) g_setVisible(Obj(g_shellSingleton), nullptr, 0);
|
|
if (BYTE* r = RadarFlag()) g_radarWas = *r != 0;
|
|
if (g_setRadarVisible && Obj(g_gameSingleton) && !Dbg("SkipRadar")) g_setRadarVisible(Obj(g_gameSingleton), nullptr, 0);
|
|
if (BYTE* l = LabelsFlag()) {
|
|
g_labelsWas = *l != 0;
|
|
if (!Dbg("SkipLabels")) *l = 0;
|
|
}
|
|
g_fovInit = false;
|
|
g_on = true;
|
|
ds2::log().Write("photo: on (%s, %s; interface hidden, shell was %s, radar was %s, names were %s)", g_mp ? "multiplayer" : "single player",
|
|
g_pausedByUs ? "game paused" : g_mp ? "game not paused" : "game was paused already", g_shellWas ? "shown" : "hidden",
|
|
g_radarWas ? "shown" : "hidden", g_labelsWas ? "shown" : "hidden");
|
|
}
|
|
|
|
void Leave(const char* why) {
|
|
if (!g_on) return;
|
|
g_on = false; // camera.cpp's next update no longer widens anything and puts the game's values back
|
|
g_fovInit = false;
|
|
g_fovWider = g_fovNarrower = false;
|
|
LeaveFree();
|
|
void* engine = Obj(g_engineSingleton);
|
|
bool sameWorld = InteractiveGame() && g_cam && engine && g_getCamera(engine, nullptr) == g_cam; // not freed by a map change
|
|
LogCamera("at the end of photo mode");
|
|
if (sameWorld && !Dbg("SkipCameraRestore") && !Dbg("SkipSiegeRestore")) {
|
|
// camera state back to what it was; the field of view and the limits are camera.cpp's (shadowed)
|
|
BYTE* c = static_cast<BYTE*>(g_cam);
|
|
std::memcpy(c + kCamFrom, g_camSave + kCamFrom, g_fovOff - kCamFrom);
|
|
std::memcpy(c + g_fovOff + 4, g_camSave + g_fovOff + 4, g_limitsOff - (g_fovOff + 4));
|
|
}
|
|
if (sameWorld && !Dbg("SkipCameraRestore") && !Dbg("SkipUiRestore") && g_uiCam && g_uiCam == Obj(g_uiCameraSingleton) && Readable(static_cast<BYTE*>(g_uiCam) + kUiFrom, kUiTo - kUiFrom))
|
|
std::memcpy(static_cast<BYTE*>(g_uiCam) + kUiFrom, g_uiSave + kUiFrom, kUiTo - kUiFrom);
|
|
if (BYTE* l = LabelsFlag()) *l = g_labelsWas ? 1 : 0;
|
|
if (g_radarWas && g_setRadarVisible && Obj(g_gameSingleton)) g_setRadarVisible(Obj(g_gameSingleton), nullptr, 1);
|
|
if (g_shellWas && g_setVisible && Obj(g_shellSingleton)) g_setVisible(Obj(g_shellSingleton), nullptr, 1);
|
|
if (g_pausedByUs && UserPaused()) SetUserPaused(false);
|
|
g_pausedByUs = false;
|
|
LogCamera("after the restore");
|
|
ds2::log().Write("photo: off (%s)", why);
|
|
}
|
|
|
|
// ---- keys --------------------------------------------------------------------------------------------------
|
|
int ParseKey(const char* s, int def) {
|
|
static const struct {
|
|
const char* name;
|
|
int vk;
|
|
} names[] = {{"insert", VK_INSERT}, {"delete", VK_DELETE}, {"home", VK_HOME}, {"end", VK_END}, {"pageup", VK_PRIOR},
|
|
{"pagedown", VK_NEXT}, {"return", VK_RETURN}, {"enter", VK_RETURN}, {"space", VK_SPACE}, {"pause", VK_PAUSE},
|
|
{"scrolllock", VK_SCROLL}, {"tab", VK_TAB}, {"backspace", VK_BACK}, {"left", VK_LEFT}, {"right", VK_RIGHT},
|
|
{"up", VK_UP}, {"down", VK_DOWN}, {"apps", VK_APPS}, {"printscreen", VK_SNAPSHOT}};
|
|
if (!*s) return def;
|
|
if (!_stricmp(s, "none")) return 0;
|
|
for (auto& n : names)
|
|
if (!_stricmp(s, n.name)) return n.vk;
|
|
if (s[0] == 'F' || s[0] == 'f') {
|
|
int n = std::atoi(s + 1);
|
|
if (n >= 1 && n <= 24) return VK_F1 + n - 1;
|
|
}
|
|
if (std::strlen(s) == 1 && std::isalnum(static_cast<unsigned char>(s[0]))) return std::toupper(static_cast<unsigned char>(s[0]));
|
|
int v = static_cast<int>(std::strtol(s, nullptr, 0));
|
|
return v > 0 && v < 256 ? v : def;
|
|
}
|
|
|
|
bool OurKey(WPARAM vk) {
|
|
if (g_free && (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D' || vk == 'R' || vk == 'F' || vk == VK_LEFT || vk == VK_RIGHT || vk == VK_UP || vk == VK_DOWN))
|
|
return true; // the free camera's own keys: the game's camera keys and potion keys do not see them
|
|
return vk != 0 && (vk == static_cast<WPARAM>(g_keyToggle) || vk == static_cast<WPARAM>(g_keyCapture) || vk == static_cast<WPARAM>(g_keyFovWider) ||
|
|
vk == static_cast<WPARAM>(g_keyFovNarrower) || vk == static_cast<WPARAM>(g_keyFree) || vk == VK_ESCAPE);
|
|
}
|
|
|
|
// The picture is taken at the hub's world-finished point (d3dhub.h), which exists in every [Display] Mode. Without it there is no
|
|
// picture (the engine's own ScreenShot, tried instead, raised an exception from inside the game): the game's own PrintScreen key
|
|
// does the same job with the interface already hidden.
|
|
void NoPicture() {
|
|
ds2::log().Write("photo: no picture: the world-finished point is missing (the Direct3D hub could not install it); the game's PrintScreen key works while photo mode is on");
|
|
}
|
|
|
|
LRESULT CALLBACK PhotoProc(HWND h, UINT msg, WPARAM wp, LPARAM lp) {
|
|
switch (msg) {
|
|
case WM_KEYDOWN:
|
|
case WM_KEYUP: {
|
|
bool down = msg == WM_KEYDOWN, repeat = down && (lp & 0x40000000);
|
|
if (wp < 256) g_keyDown[wp] = down;
|
|
if (wp == static_cast<WPARAM>(g_keyToggle) && g_keyToggle) { // always ours: the key is not passed on
|
|
if (down && !repeat) {
|
|
if (g_on) Leave("key");
|
|
else Enter();
|
|
}
|
|
return 0;
|
|
}
|
|
if (!g_on || !OurKey(wp)) break;
|
|
if (down && !repeat) {
|
|
if (wp == static_cast<WPARAM>(g_keyCapture)) {
|
|
if (g_hooked) InterlockedExchange(&g_shotRequested, 1);
|
|
else NoPicture();
|
|
}
|
|
else if (wp == VK_ESCAPE) Leave("escape");
|
|
else if (wp == static_cast<WPARAM>(g_keyFree) && g_keyFree) {
|
|
if (g_free) LeaveFree();
|
|
else EnterFree();
|
|
}
|
|
}
|
|
if (wp == static_cast<WPARAM>(g_keyFovWider)) g_fovWider = down;
|
|
if (wp == static_cast<WPARAM>(g_keyFovNarrower)) g_fovNarrower = down;
|
|
return 0;
|
|
}
|
|
case WM_CHAR:
|
|
if (g_on && wp == '\r' && g_keyCapture == VK_RETURN) return 0;
|
|
break;
|
|
case WM_LBUTTONDOWN:
|
|
case WM_LBUTTONDBLCLK:
|
|
case WM_RBUTTONDOWN:
|
|
case WM_RBUTTONDBLCLK:
|
|
if (g_on) return 0; // a click would order the party about
|
|
break;
|
|
case WM_MOUSEWHEEL: // free camera: the wheel flies forward / back
|
|
if (g_free) {
|
|
g_wheel += static_cast<float>(static_cast<short>(HIWORD(wp))) / 120.0f;
|
|
return 0;
|
|
}
|
|
break;
|
|
case WM_MBUTTONDOWN: // free camera: middle button + drag looks around
|
|
case WM_MBUTTONUP:
|
|
if (g_free) {
|
|
g_look = msg == WM_MBUTTONDOWN;
|
|
g_lookLast = {static_cast<short>(LOWORD(lp)), static_cast<short>(HIWORD(lp))};
|
|
return 0;
|
|
}
|
|
break;
|
|
case WM_MOUSEMOVE:
|
|
if (g_free && g_look) {
|
|
POINT now = {static_cast<short>(LOWORD(lp)), static_cast<short>(HIWORD(lp))};
|
|
g_mouseDx += static_cast<float>(now.x - g_lookLast.x);
|
|
g_mouseDy += static_cast<float>(now.y - g_lookLast.y);
|
|
g_lookLast = now;
|
|
}
|
|
break;
|
|
case WM_KILLFOCUS:
|
|
std::memset(g_keyDown, 0, sizeof(g_keyDown));
|
|
g_fovWider = g_fovNarrower = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return CallWindowProcA(g_orig, h, msg, wp, lp);
|
|
}
|
|
|
|
BOOL CALLBACK FindGameWindow(HWND h, LPARAM out) {
|
|
char cls[64];
|
|
if (GetClassNameA(h, cls, sizeof(cls)) && _strnicmp(cls, "gpgwndclass_", 12) == 0) {
|
|
*reinterpret_cast<HWND*>(out) = h;
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
void HookWindow() {
|
|
if (g_wnd) return;
|
|
HWND h = nullptr;
|
|
EnumThreadWindows(GetCurrentThreadId(), FindGameWindow, reinterpret_cast<LPARAM>(&h));
|
|
if (!h) return;
|
|
g_orig = reinterpret_cast<WNDPROC>(SetWindowLongA(h, GWL_WNDPROC, reinterpret_cast<LONG>(PhotoProc)));
|
|
if (g_orig) {
|
|
g_wnd = h;
|
|
ds2::log().Write("photo: keys hooked on window %p", static_cast<void*>(h));
|
|
}
|
|
}
|
|
|
|
// ---- capture ---------------------------------------------------------------------------------------------
|
|
// The engine's screenshot folder: RapiAppModule holds the expanded "%shots_path%" (".../Screenshots") as a wide string
|
|
// (set where the game builds its paths, Steam sub_430f0d -> sub_4305d8, object +0x1d0). The offset is found by looking for
|
|
// that string so the same code works in both exes.
|
|
bool ShotsFolder(wchar_t* out, size_t cap) {
|
|
static DWORD off = 0;
|
|
BYTE* rapi = static_cast<BYTE*>(Obj(g_rapiSingleton));
|
|
if (!rapi) return false;
|
|
auto good = [&](DWORD o) {
|
|
const wchar_t* s = *reinterpret_cast<const wchar_t* const*>(rapi + o);
|
|
if (!Readable(s, 4)) return false;
|
|
size_t n = 0;
|
|
while (n < 600 && Readable(s + n, 2) && s[n]) ++n;
|
|
if (n < 12 || n >= 600 || !Readable(s, (n + 1) * 2)) return false;
|
|
return _wcsicmp(s + n - 11, L"Screenshots") == 0;
|
|
};
|
|
if (off && !good(off)) off = 0;
|
|
for (DWORD o = 0x40; !off && o < 0x400; o += 4)
|
|
if (Readable(rapi + o, 4) && good(o)) off = o;
|
|
if (!off) return false;
|
|
wcsncpy_s(out, cap, *reinterpret_cast<const wchar_t* const*>(rapi + off), _TRUNCATE);
|
|
return true;
|
|
}
|
|
|
|
using SaveW_t = HRESULT(WINAPI*)(const wchar_t*, DWORD, IDirect3DSurface9*, const PALETTEENTRY*, const RECT*);
|
|
|
|
SaveW_t FindD3dxSave() {
|
|
static SaveW_t save = nullptr;
|
|
for (int v = 43; !save && v >= 24; --v) {
|
|
char dll[32];
|
|
sprintf_s(dll, "d3dx9_%d.dll", v);
|
|
HMODULE m = GetModuleHandleA(dll);
|
|
if (!m && v != 29) continue; // the game loads d3dx9_29; Broken World links D3DX statically
|
|
if (!m) m = LoadLibraryA(dll);
|
|
if (m) save = reinterpret_cast<SaveW_t>(GetProcAddress(m, "D3DXSaveSurfaceToFileW"));
|
|
}
|
|
for (int v = 43; !save && v >= 24; --v) {
|
|
char dll[32];
|
|
sprintf_s(dll, "d3dx9_%d.dll", v);
|
|
if (HMODULE m = LoadLibraryA(dll)) save = reinterpret_cast<SaveW_t>(GetProcAddress(m, "D3DXSaveSurfaceToFileW"));
|
|
}
|
|
return save;
|
|
}
|
|
|
|
void TakePicture(IDirect3DDevice9* dev) {
|
|
wchar_t dir[MAX_PATH], path[MAX_PATH + 64];
|
|
if (!ShotsFolder(dir, MAX_PATH)) {
|
|
ds2::log().Write("photo: the engine's screenshot folder was not found - no picture");
|
|
return;
|
|
}
|
|
CreateDirectoryW(dir, nullptr);
|
|
SYSTEMTIME t;
|
|
GetLocalTime(&t);
|
|
swprintf_s(path, L"%ls\\DS2 Photo %04d-%02d-%02d %02d-%02d-%02d.png", dir, t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond);
|
|
for (int n = 2; GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES && n < 100; ++n)
|
|
swprintf_s(path, L"%ls\\DS2 Photo %04d-%02d-%02d %02d-%02d-%02d (%d).png", dir, t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond, n);
|
|
IDirect3DSurface9 *rt = nullptr, *resolved = nullptr, *sys = nullptr;
|
|
HRESULT hr = dev->GetRenderTarget(0, &rt);
|
|
D3DSURFACE_DESC d{};
|
|
if (SUCCEEDED(hr)) hr = rt->GetDesc(&d);
|
|
IDirect3DSurface9* src = rt;
|
|
if (SUCCEEDED(hr) && d.MultiSampleType != D3DMULTISAMPLE_NONE) {
|
|
hr = dev->CreateRenderTarget(d.Width, d.Height, d.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &resolved, nullptr);
|
|
if (SUCCEEDED(hr)) hr = dev->StretchRect(rt, nullptr, resolved, nullptr, D3DTEXF_POINT);
|
|
src = resolved;
|
|
}
|
|
if (SUCCEEDED(hr)) hr = dev->CreateOffscreenPlainSurface(d.Width, d.Height, d.Format, D3DPOOL_SYSTEMMEM, &sys, nullptr);
|
|
if (SUCCEEDED(hr)) hr = dev->GetRenderTargetData(src, sys);
|
|
if (SUCCEEDED(hr) && d.Format == D3DFMT_A8R8G8B8) { // opaque picture
|
|
D3DLOCKED_RECT lr{};
|
|
if (SUCCEEDED(sys->LockRect(&lr, nullptr, 0))) {
|
|
for (UINT y = 0; y < d.Height; ++y) {
|
|
DWORD* row = reinterpret_cast<DWORD*>(static_cast<BYTE*>(lr.pBits) + static_cast<size_t>(y) * lr.Pitch);
|
|
for (UINT x = 0; x < d.Width; ++x) row[x] |= 0xFF000000u;
|
|
}
|
|
sys->UnlockRect();
|
|
}
|
|
}
|
|
SaveW_t save = SUCCEEDED(hr) ? FindD3dxSave() : nullptr;
|
|
if (SUCCEEDED(hr) && save) hr = save(path, 3 /* D3DXIFF_PNG */, sys, nullptr, nullptr);
|
|
else if (SUCCEEDED(hr)) hr = E_FAIL;
|
|
char u8[MAX_PATH * 4];
|
|
WideCharToMultiByte(CP_UTF8, 0, path, -1, u8, sizeof(u8), nullptr, nullptr);
|
|
if (SUCCEEDED(hr)) ds2::log().Write("photo: picture %ux%u saved: %s", d.Width, d.Height, u8);
|
|
else ds2::log().Write("photo: picture failed (0x%08lX, D3DX %s): %s", static_cast<unsigned long>(hr), save ? "found" : "missing", u8);
|
|
if (sys) sys->Release();
|
|
if (resolved) resolved->Release();
|
|
if (rt) rt->Release();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace widescreen::photo {
|
|
|
|
bool Wanted() { return g_wanted; }
|
|
bool Active() { return g_on; }
|
|
void NoZoom() { g_zoom = 1.0f; }
|
|
|
|
void Install(const ds2::Ini& ini) {
|
|
g_ini = new ds2::Ini(ini);
|
|
char v[32];
|
|
auto key = [&](const char* name, int def) {
|
|
ini.String("Photo", name, "", v, sizeof(v));
|
|
return ParseKey(v, def);
|
|
};
|
|
auto num = [&](const char* name, float def, float lo, float hi) {
|
|
ini.String("Photo", name, "", v, sizeof(v));
|
|
float f = v[0] ? static_cast<float>(std::atof(v)) : def;
|
|
return f < lo ? lo : f > hi ? hi : f;
|
|
};
|
|
g_keyToggle = key("Key", VK_INSERT);
|
|
g_keyCapture = key("CaptureKey", VK_RETURN);
|
|
g_keyFovWider = key("FovWiderKey", VK_PRIOR);
|
|
g_keyFovNarrower = key("FovNarrowerKey", VK_NEXT);
|
|
g_keyFree = key("FreeCameraKey", VK_DELETE);
|
|
g_zoom = num("MaxZoom", 3.0f, 1.0f, 5.0f);
|
|
g_minPitch = num("MinPitch", 10.0f, 5.0f, 60.0f);
|
|
g_maxPitch = num("MaxPitch", 89.0f, 60.0f, 89.0f);
|
|
g_fovMin = num("FovMin", 20.0f, 10.0f, 60.0f);
|
|
g_fovMax = num("FovMax", 110.0f, 60.0f, 140.0f);
|
|
g_fovSpeed = num("FovSpeed", 30.0f, 5.0f, 120.0f);
|
|
g_freeSpeed = num("FreeCameraSpeed", 6.0f, 0.5f, 60.0f);
|
|
ini.String("Photo", "HideSelection", "1", v, sizeof(v));
|
|
g_hideSelection = std::atoi(v) != 0;
|
|
g_freeRadius = num("FreeCameraRadius", 0.0f, 0.0f, 500.0f);
|
|
if (!g_keyToggle) {
|
|
ds2::log().Write("photo: off ([Photo] Key=none)");
|
|
return;
|
|
}
|
|
g_engineSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@SiegeEngine@siege@@CAPAV12@XZ");
|
|
g_getCamera = Export<GetCamera_t>("?GetCamera@SiegeEngine@siege@@QBEABVSiegeCamera@2@XZ");
|
|
g_serverSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@Server@@CAPAV1@XZ");
|
|
g_worldMapSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@WorldMap@@CAPAV1@XZ");
|
|
g_getScreenHero = Export<PtrThis_t>("?GetScreenHero@Server@@QAEPAVGo@@XZ");
|
|
g_getPlacement = Export<PtrThis_t>("?GetPlacement@Go@@QAEPAVGoPlacement@@XZ");
|
|
g_getPosition = Export<PtrThis_t>("?GetPosition@GoPlacement@@QBEABUSiegePos@@XZ");
|
|
g_frustumRadius = Export<FloatThis_t>("?GetWorldFrustumRadius@WorldMap@@QBEMXZ");
|
|
g_convert = Export<Convert_t>("?ConvertSiegePosToNode@@YAXAAUSiegePos@@ABVdatabase_guid@siege@@@Z");
|
|
auto getterOffset = [](const char* name) -> DWORD { // "lea eax,[ecx+disp32]; ret"
|
|
auto p = reinterpret_cast<const BYTE*>(GetProcAddress(GetModuleHandleA(nullptr), name));
|
|
return p && p[0] == 0x8D && p[1] == 0x81 && p[6] == 0xC3 ? *reinterpret_cast<const DWORD*>(p + 2) : 0;
|
|
};
|
|
o_camSiegePos = getterOffset("?GetCameraSiegePos@SiegeCamera@siege@@QBEABUSiegePos@@XZ");
|
|
o_tgtSiegePos = getterOffset("?GetTargetSiegePos@SiegeCamera@siege@@QBEABUSiegePos@@XZ");
|
|
if (!g_convert || !o_camSiegePos || !o_tgtSiegePos || !g_serverSingleton || !g_getScreenHero || !g_getPlacement || !g_getPosition)
|
|
ds2::log().Write("photo: free camera unavailable (engine entry points missing: convert %p, camera SiegePos +%lX, target +%lX)", reinterpret_cast<void*>(g_convert),
|
|
o_camSiegePos, o_tgtSiegePos);
|
|
g_appSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@AppModule@@CAPAV1@XZ");
|
|
g_shellSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@UIShell@@CAPAV1@XZ");
|
|
g_gameSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@UIGame@@CAPAV1@XZ");
|
|
g_uiCameraSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@UICamera@@CAPAV1@XZ");
|
|
g_worldStateSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@WorldState@@CAPAV1@XZ");
|
|
g_rapiSingleton = Export<Singleton_t>("?FUBI_GetClassSingleton@RapiAppModule@@CAPAV1@XZ");
|
|
g_userPause = Export<ThisBool_t>("?UserPause@AppModule@@QAEX_N@Z");
|
|
g_isUserPaused = Export<ThisRet_t>("?IsUserPaused@AppModule@@QAE_NXZ");
|
|
g_setVisible = Export<ThisBool_t>("?SetVisible@UIShell@@QAEX_N@Z");
|
|
g_setRadarVisible = Export<ThisBool_t>("?SetRadarVisible@UIGame@@QAEX_N@Z");
|
|
g_currentState = Export<ThisRet_t>("?GetCurrentState@WorldState@@QBE?AW4eWorldState@@XZ");
|
|
g_inGameInteractive = Export<FreeInt_t>("?IsInGameInteractive@@YA_NW4eWorldState@@@Z");
|
|
g_isMultiPlayer = Export<Free_t>("?IsMultiPlayer@@YA_NXZ");
|
|
if (!g_engineSingleton || !g_getCamera || !g_appSingleton || !g_shellSingleton || !g_gameSingleton || !g_uiCameraSingleton || !g_worldStateSingleton || !g_rapiSingleton ||
|
|
!g_userPause || !g_isUserPaused || !g_setVisible || !g_setRadarVisible || !g_currentState || !g_inGameInteractive || !g_isMultiPlayer) {
|
|
ds2::log().Write("SKIP photo: an engine export is missing - photo mode off");
|
|
return;
|
|
}
|
|
// UIShell::SetVisible = "mov al,[ebp+8]; mov [ecx+disp32],al" (Steam 0x425c3d, Broken World 0x4281a2)
|
|
auto sv = reinterpret_cast<const BYTE*>(g_setVisible);
|
|
if (sv[0] == 0x55 && sv[1] == 0x8B && sv[2] == 0xEC && sv[3] == 0x8A && sv[4] == 0x45 && sv[5] == 0x08 && sv[6] == 0x88 && sv[7] == 0x81)
|
|
o_shellVisible = *reinterpret_cast<const DWORD*>(sv + 8);
|
|
// Selection rings: the ring drawing (Steam sub_46fdd0) starts with the tests "options flag, UIGame +0x85"; the offset is read from it.
|
|
DWORD ring = ds2::FindUnique("55 8B EC 83 EC 2C A1 ?? ?? ?? ?? 57 33 FF 39 3D ?? ?? ?? ?? 89 45 FC 89 4D E0 0F 84 ?? ?? ?? ?? A1 ?? ?? ?? ?? 80 78 53 00 0F 84 ?? ?? ?? ?? 80 B9 ?? ?? 00 00 00");
|
|
if (ring) o_ringOff = *reinterpret_cast<const DWORD*>(ring + 49);
|
|
// The hover outline: its quad is drawn from the siege render (Steam 0x6db0c9, "push esi; call <quad>; add esp,0x1c").
|
|
DWORD outline = ds2::FindUnique("0B D0 52 8B 96 ?? ?? 00 00 51 52 6A 00 6A 00 56 E8 ?? ?? ?? ?? 83 C4 1C 8B CE");
|
|
if (outline) {
|
|
DWORD site = outline + 16, target = site + 5 + *reinterpret_cast<const LONG*>(site + 1);
|
|
g_outlineQuad = reinterpret_cast<OutlineQuad_t>(target);
|
|
if (!ds2::RedirectCall(site, target, reinterpret_cast<DWORD>(&HookOutlineQuad), "photo: hover outline quad (hidden in the picture)")) g_outlineQuad = nullptr;
|
|
}
|
|
ds2::log().Write("photo: selection rings %s (UIGame +%lX), hover outline %s; HideSelection=%d", o_ringOff ? "found" : "NOT found", o_ringOff, g_outlineQuad ? "found" : "NOT found",
|
|
g_hideSelection ? 1 : 0);
|
|
// The interface pass (hud.cpp finds it the same way): "mov ecx,[esi+0x4c]" is the radar, "cmp byte [esi+disp32],0" the
|
|
// multiplayer names switch.
|
|
DWORD pass = ds2::FindUnique("A1 ?? ?? ?? ?? 83 78 1C 2C 56 8B F1 74 ?? 8B 4E 3C E8 ?? ?? ?? ?? 8B 4E 4C E8 ?? ?? ?? ?? A1");
|
|
if (pass) {
|
|
auto p = reinterpret_cast<const BYTE*>(pass);
|
|
o_radar = p[0x18];
|
|
for (int i = 0x20; i < 0x60 && !o_labels; ++i)
|
|
if (p[i] == 0x80 && p[i + 1] == 0xBE && p[i + 4] == 0 && p[i + 5] == 0 && p[i + 6] == 0) o_labels = *reinterpret_cast<const DWORD*>(p + i + 2);
|
|
}
|
|
ds2::log().Write("photo: key %d (capture %d), UIShell flag +%lX, UIGame radar +%lX, names +%lX; max zoom x%.1f, pitch %.0f..%.0f", g_keyToggle,
|
|
g_keyCapture, o_shellVisible, o_radar, o_labels, g_zoom, g_minPitch, g_maxPitch);
|
|
if (!o_shellVisible) {
|
|
ds2::log().Write("SKIP photo: UIShell::SetVisible not as expected - photo mode off");
|
|
return;
|
|
}
|
|
g_wanted = true;
|
|
}
|
|
|
|
// camera.cpp, every camera update: the camera is known, keys are hooked, photo mode ends with the game.
|
|
void OnUpdate(void* cam, DWORD fovOff, DWORD limitsOff) {
|
|
if (!g_wanted) return;
|
|
g_fovOff = fovOff;
|
|
g_limitsOff = limitsOff;
|
|
if (!g_wnd) HookWindow();
|
|
if (!g_on) return;
|
|
if (!InteractiveGame()) { // the game ended, a cutscene began, the player left to the menu
|
|
Leave("the game is no longer interactive");
|
|
return;
|
|
}
|
|
if (BYTE* f = ShellFlag()) *f = 0; // the interface stays hidden whatever the game does
|
|
if (g_free && cam == g_cam) FreeStep(cam);
|
|
}
|
|
|
|
// camera.cpp: widened limits while photo mode is on (widen only).
|
|
bool Limits(void* cam, float* zoom, float* minPitch, float* maxPitch) {
|
|
if (!g_on || cam != g_cam) return false; // the main camera only: the interface's 3D views keep the game's limits
|
|
if (g_zoom > *zoom) *zoom = g_zoom;
|
|
*minPitch = *minPitch > 0 && *minPitch < g_minPitch ? *minPitch : g_minPitch;
|
|
if (g_maxPitch > *maxPitch) *maxPitch = g_maxPitch;
|
|
return true;
|
|
}
|
|
|
|
// camera.cpp: the field of view while photo mode is on. gameFov is what the camera would have without photo mode
|
|
// (horizontal radians); the photo mode keeps its own vertical angle, starting at the one in use.
|
|
void Fov(float gameFov, float aspect, float* out) {
|
|
LARGE_INTEGER now, freq;
|
|
QueryPerformanceCounter(&now);
|
|
QueryPerformanceFrequency(&freq);
|
|
if (!g_fovInit) {
|
|
g_fovV = 2.0f * std::atan(std::tan(gameFov * 0.5f) / aspect) / kDeg;
|
|
g_fovInit = true;
|
|
g_fovT = now;
|
|
ds2::log().Write("photo: field of view starts at %.1f deg vertical (%.1f horizontal, aspect %.3f)", g_fovV, gameFov / kDeg, aspect);
|
|
}
|
|
float dt = static_cast<float>(now.QuadPart - g_fovT.QuadPart) / static_cast<float>(freq.QuadPart);
|
|
if (dt > 0.001f) { // the camera update runs several times per frame
|
|
g_fovT = now;
|
|
if (dt > 0.1f) dt = 0.1f;
|
|
float old = g_fovV;
|
|
if (g_fovWider) g_fovV += g_fovSpeed * dt;
|
|
if (g_fovNarrower) g_fovV -= g_fovSpeed * dt;
|
|
if (g_fovV < g_fovMin) g_fovV = g_fovMin;
|
|
if (g_fovV > g_fovMax) g_fovV = g_fovMax;
|
|
if (std::fabs(g_fovV - old) > 1e-4f && (static_cast<int>(old / 10.0f) != static_cast<int>(g_fovV / 10.0f)))
|
|
ds2::log().Write("photo: field of view %.0f deg vertical", g_fovV);
|
|
}
|
|
float h = 2.0f * std::atan(std::tan(g_fovV * kDeg * 0.5f) * aspect);
|
|
*out = h > 170.0f * kDeg ? 170.0f * kDeg : h;
|
|
}
|
|
|
|
// hud.cpp, at the interface pass: the 3D world is finished (anti-aliased), the interface not drawn yet.
|
|
void WorldDone(IDirect3DDevice9* dev) {
|
|
g_hooked = true;
|
|
if (g_shotStage == 0) {
|
|
if (!InterlockedExchange(&g_shotRequested, 0)) return;
|
|
if (!g_on || !dev) return;
|
|
if (g_ini) g_hideSelection = g_ini->Bool("Photo", "HideSelection", true); // read at every picture: a settings file edit takes effect at once
|
|
BYTE* ring = o_ringOff && g_hideSelection ? static_cast<BYTE*>(Obj(g_gameSingleton)) : nullptr;
|
|
if (ring) ring += o_ringOff;
|
|
if (!ring && !(g_outlineQuad && g_hideSelection)) {
|
|
TakePicture(dev);
|
|
return;
|
|
}
|
|
if (ring) g_ringWas = *ring, *ring = 1;
|
|
g_outlineHidden = g_outlineQuad && g_hideSelection;
|
|
g_shotStage = 1; // the next frame draws without them; the picture follows one frame later
|
|
return;
|
|
}
|
|
if (++g_shotStage < 3) return;
|
|
g_shotStage = 0;
|
|
if (g_on && dev) TakePicture(dev);
|
|
BYTE* ring = o_ringOff ? static_cast<BYTE*>(Obj(g_gameSingleton)) : nullptr;
|
|
if (ring) ring[o_ringOff] = g_ringWas;
|
|
g_outlineHidden = false;
|
|
}
|
|
|
|
} // namespace widescreen::photo
|
|
|
|
// For other plugins (the controller, later): is photo mode on? Non-zero while it is; its world actions (a click, a walk order) are
|
|
// not delivered as window messages and so cannot be swallowed here.
|
|
#pragma comment(linker, "/EXPORT:DS2PhotoMode=_DS2PhotoMode@0")
|
|
extern "C" int __stdcall DS2PhotoMode() { return g_on ? 1 : 0; }
|