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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Drawing the scanned entities into the world.
//
// The projection has to match the one the game rendered with, or the boxes sit
// beside the things they are tracking. Minecraft's yaw is zero looking south
// (+Z) and increases clockwise, so forward is
// (-sin yaw · cos pitch, -sin pitch, cos yaw · cos pitch); right is forward
// crossed with world-up, and the rest is an ordinary pinhole projection.
//
// One known limitation: this uses the *player's* eye and rotation rather than
// the render camera. They are the same in first person, which is the case that
// matters now; third person and freecam will need GameRenderer.mainCamera
// bound before they line up.
#pragma once
#include <cmath>
#include <string>
#include "imgui.h"
#include "state.hpp"
namespace lodestone::esp {
namespace {
ImU32 col(const state::Colour& c) {
return ImGui::ColorConvertFloat4ToU32(ImVec4(c[0], c[1], c[2], c[3]));
}
double dot(const state::Vec3& a, const state::Vec3& b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
/// World-to-screen for one frame's camera.
struct View {
state::Vec3 camera, forward, right, up;
float half_w = 0, half_h = 0;
double tan_half_fov = 1.0, aspect = 1.0;
View(const state::Vec3& eye, float yaw, float pitch, float fov, const ImVec2& size) {
camera = eye;
double y = yaw * 3.14159265358979323846 / 180.0;
double p = pitch * 3.14159265358979323846 / 180.0;
double sy = std::sin(y), cy = std::cos(y), sp = std::sin(p), cp = std::cos(p);
forward = {-sy * cp, -sp, cy * cp};
// forward x (0,1,0), flattened and normalised.
state::Vec3 r{-forward.z, 0.0, forward.x};
double rl = std::sqrt(r.x * r.x + r.z * r.z);
if (rl < 1e-9) rl = 1e-9;
right = {r.x / rl, 0.0, r.z / rl};
// up = right x forward
up = {right.y * forward.z - right.z * forward.y, right.z * forward.x - right.x * forward.z,
right.x * forward.y - right.y * forward.x};
half_w = size.x * 0.5f;
half_h = size.y * 0.5f;
double f = fov < 1.0f ? 1.0 : static_cast<double>(fov);
tan_half_fov = std::tan(f * 0.5 * 3.14159265358979323846 / 180.0);
aspect = size.y > 1.0f ? static_cast<double>(size.x) / static_cast<double>(size.y) : 1.0;
}
bool project(const state::Vec3& world, ImVec2* out) const {
state::Vec3 d{world.x - camera.x, world.y - camera.y, world.z - camera.z};
double z = dot(d, forward);
if (z <= 0.05) return false; // at or behind the eye
double x = dot(d, right);
double y = dot(d, up);
out->x = half_w * static_cast<float>(1.0 + x / (z * tan_half_fov * aspect));
out->y = half_h * static_cast<float>(1.0 - y / (z * tan_half_fov));
return true;
}
/// The screen rectangle covering all eight corners of a world box.
bool project_box(const state::Vec3& lo, const state::Vec3& hi, ImVec2* out_min,
ImVec2* out_max) const {
ImVec2 mn(1e9f, 1e9f), mx(-1e9f, -1e9f);
for (int i = 0; i < 8; ++i) {
state::Vec3 corner{(i & 1) ? hi.x : lo.x, (i & 2) ? hi.y : lo.y, (i & 4) ? hi.z : lo.z};
ImVec2 p;
if (!project(corner, &p)) return false; // any corner behind: skip it
mn.x = p.x < mn.x ? p.x : mn.x;
mn.y = p.y < mn.y ? p.y : mn.y;
mx.x = p.x > mx.x ? p.x : mx.x;
mx.y = p.y > mx.y ? p.y : mx.y;
}
*out_min = mn;
*out_max = mx;
return true;
}
};
bool shows(const state::Esp& cfg, state::TargetKind kind) {
switch (kind) {
case state::TargetKind::Player: return cfg.players;
case state::TargetKind::Mob: return cfg.mobs;
case state::TargetKind::Animal: return cfg.animals;
case state::TargetKind::Item: return cfg.items;
case state::TargetKind::Other: return false;
}
return false;
}
const state::Colour& colour_of(const state::Esp& cfg, const state::Target& t) {
// Something whose own reach already covers you is the one thing worth
// seeing first, so it outranks every other colour.
if (t.can_reach_you && cfg.show_threat) return cfg.color_threat;
if (t.invisible && cfg.show_invis) return cfg.color_invis;
switch (t.kind) {
case state::TargetKind::Player: return cfg.color_player;
case state::TargetKind::Mob: return cfg.color_mob;
case state::TargetKind::Animal: return cfg.color_animal;
case state::TargetKind::Item: return cfg.color_item;
case state::TargetKind::Other: break;
}
return cfg.color_player;
}
void draw_box(ImDrawList* dl, const ImVec2& a, const ImVec2& b, ImU32 c, state::BoxStyle style,
bool fill) {
if (style == state::BoxStyle::Corners) {
float lx = (b.x - a.x) * 0.28f, ly = (b.y - a.y) * 0.22f;
const float corners[4][4] = {
{a.x, a.y, 1.0f, 1.0f}, {b.x, a.y, -1.0f, 1.0f},
{a.x, b.y, 1.0f, -1.0f}, {b.x, b.y, -1.0f, -1.0f}};
for (const auto& k : corners) {
dl->AddLine(ImVec2(k[0], k[1]), ImVec2(k[0] + lx * k[2], k[1]), c, 1.5f);
dl->AddLine(ImVec2(k[0], k[1]), ImVec2(k[0], k[1] + ly * k[3]), c, 1.5f);
}
return;
}
if (fill || style == state::BoxStyle::Filled) {
ImVec4 f = ImGui::ColorConvertU32ToFloat4(c);
f.w = 0.13f;
dl->AddRectFilled(a, b, ImGui::ColorConvertFloat4ToU32(f));
}
// A dark outline keeps the box readable against bright terrain.
dl->AddRect(ImVec2(a.x - 1, a.y - 1), ImVec2(b.x + 1, b.y + 1), IM_COL32(0, 0, 0, 150));
dl->AddRect(a, b, c);
}
} // namespace
/// Draw the world overlay. Runs whether or not the menu is open.
inline void draw(const state::Shared& s) {
const state::Esp& cfg = s.cfg.esp;
const state::GameState& game = s.game;
if (!game.in_world || game.targets.empty()) return;
ImVec2 size = ImGui::GetIO().DisplaySize;
if (size.x < 1.0f || size.y < 1.0f) return;
// What the world was actually rendered with, unless the custom-FOV module
// is overriding it.
float fov = s.cfg.visuals.fov ? s.cfg.visuals.fov_value
: (game.fov > 1.0f ? game.fov : 70.0f);
View view(game.eye, game.yaw, game.pitch, fov, size);
ImDrawList* dl = ImGui::GetBackgroundDrawList();
for (const auto& t : game.targets) {
if (!shows(cfg, t.kind)) continue;
if (t.distance > cfg.distance) continue;
ImVec2 a, b;
if (!view.project_box(t.min, t.max, &a, &b)) continue;
if (b.x < 0 || b.y < 0 || a.x > size.x || a.y > size.y) continue;
ImU32 c = col(colour_of(cfg, t));
if (cfg.boxes) draw_box(dl, a, b, c, cfg.box_style, cfg.box_fill);
if (cfg.tracers) {
dl->AddLine(ImVec2(size.x * 0.5f, size.y), ImVec2((a.x + b.x) * 0.5f, b.y), c);
}
if (cfg.health_bars && t.max_health > 0.0f) {
float frac = t.health / t.max_health;
frac = frac < 0 ? 0 : (frac > 1 ? 1 : frac);
float x = a.x - 5.0f;
dl->AddRectFilled(ImVec2(x - 1, a.y), ImVec2(x + 1.5f, b.y), IM_COL32(0, 0, 0, 150));
float top = b.y - (b.y - a.y) * frac;
ImU32 hc = IM_COL32(static_cast<int>(255 * (1.0f - frac)), static_cast<int>(220 * frac),
80, 255);
dl->AddRectFilled(ImVec2(x - 1, top), ImVec2(x + 1.5f, b.y), hc);
}
if (cfg.nametags && !t.name.empty()) {
char label[128];
if (cfg.show_ping && t.ping >= 0) {
std::snprintf(label, sizeof(label), "%s %.0fm %dms", t.name.c_str(),
static_cast<double>(t.distance), t.ping);
} else {
std::snprintf(label, sizeof(label), "%s %.0fm", t.name.c_str(),
static_cast<double>(t.distance));
}
float tw = ImGui::CalcTextSize(label).x;
float cx = (a.x + b.x) * 0.5f;
dl->AddRectFilled(ImVec2(cx - tw * 0.5f - 3, a.y - 16), ImVec2(cx + tw * 0.5f + 3, a.y - 2),
IM_COL32(0, 0, 0, 150), 2.0f);
dl->AddText(ImVec2(cx - tw * 0.5f, a.y - 15), c, label);
}
// What they are holding and wearing, above the name. Armour is shown
// even with an empty hand — bare-handed in full diamond still matters.
if (cfg.show_gear && (!t.held.empty() || t.armor > 0 || !t.effects.empty())) {
std::string gear = t.held;
if (t.armor > 0) {
char armour[16];
std::snprintf(armour, sizeof(armour), "[%da]", t.armor);
if (!gear.empty()) gear += " ";
gear += armour;
}
// Only the first few: a long potion stack would cover the screen.
for (size_t i = 0; i < t.effects.size() && i < 3; ++i) {
gear += (gear.empty() ? "" : " ");
gear += t.effects[i];
}
float gw = ImGui::CalcTextSize(gear.c_str()).x;
float cx = (a.x + b.x) * 0.5f;
dl->AddText(ImVec2(cx - gw * 0.5f + 1, a.y - 29), IM_COL32(0, 0, 0, 160), gear.c_str());
dl->AddText(ImVec2(cx - gw * 0.5f, a.y - 30), IM_COL32(200, 200, 210, 255),
gear.c_str());
}
}
}
} // namespace lodestone::esp