Large map: explored border as a continuous line along the fog edge
The game drew 2x2 px red dots per visited-sector neighbour (0x6c8580), in 64-tile steps and unrelated to the explored mask. Replaced by lines traced from the explored cells (2x2 tiles), simplified to 0.75 px, rebuilt while the map is open when the masks change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7d8089c299
commit
32f953fc01
8 files changed
+496
-21
No files matched your search
@@ -76,6 +76,7 @@ add_library(sacredK SHARED
|
||||
src/mods/screen.cpp
|
||||
src/mods/characters.cpp
|
||||
src/mods/mainmenu.cpp
|
||||
src/mods/mapborder.cpp
|
||||
src/mods/mapreveal.cpp
|
||||
src/mods/questbook.cpp
|
||||
src/mods/followers.cpp
|
||||
|
||||
@@ -42,3 +42,24 @@ default (0x6ce677), 300 where 0x63d280 holds for the hero's area (0x6ce6b5),
|
||||
0x182cdb8 & 2 shows them. The mod scales the three base values by 10 as well
|
||||
(about 120 tiles by day). The view only lists creatures of the sectors around
|
||||
the hero (25 entries at [esp+1ech]).
|
||||
|
||||
## Border line on the large map (2026-10-26)
|
||||
|
||||
"The red outline on the mega map is choppy and feels weird."
|
||||
|
||||
Measured (disassembly): `cUI_Megamap::DrawVisitedBorder` 0x6c8580 drew a
|
||||
2x2 px red dot (0xffff0000) per bit of a 100x100 byte grid (world +0xb73c):
|
||||
unvisited sectors next to visited ones (sector flag 0x20000, set when the
|
||||
hero's sector becomes the centre of the active 3x3, 0x626603), one bit per
|
||||
visited neighbour, the dot 10 / 20 px from the sector origin towards it.
|
||||
Built on load by 0x633230 and per sector change by 0x6332d0 / 0x6334f0. So:
|
||||
dots in 64-tile steps, following the sectors stood in rather than the
|
||||
explored mask the fog shows.
|
||||
|
||||
Mod (`mods/mapborder.cpp`): the draw is replaced. The sides between explored
|
||||
and unexplored cells (2x2 tiles) of the 100x100 map sectors are chained into
|
||||
polylines (clockwise around explored cells; at a corner touch the chain turns
|
||||
right), projected with `WorldToMapPixel` (linear), simplified to 0.75 px
|
||||
(Douglas-Peucker) and drawn as 2 px red lines. Rebuilt while the map is open
|
||||
when the masks change (checked every 250 ms). Cells of missing sectors count
|
||||
as neither explored nor unexplored, so no line runs along the world's edge.
|
||||
@@ -14,6 +14,38 @@ namespace game {
|
||||
// The world's sector grid (ecx of World_SetExplored).
|
||||
inline void* World() { return *reinterpret_cast<void**>(0x00ad14e0); }
|
||||
|
||||
// The sector grid: 128x128 sectors of 64x64 tiles (tiles < 0x2000), a u16
|
||||
// index per sector at +0x284 (row-major, 0 = none) into the sector pointer
|
||||
// vector at +0x240 (World_SetExplored 0x638f00).
|
||||
constexpr int kSectorTiles = 64;
|
||||
constexpr int kSectorGrid = 128;
|
||||
constexpr int kExploredCellTiles = 2; // one mask bit per 2x2 tiles
|
||||
constexpr int kSectorCells = kSectorTiles / kExploredCellTiles; // 32
|
||||
constexpr size_t kSectorExploredMask = 0x180; // 32 rows of 4 bytes
|
||||
|
||||
inline const uint8_t* SectorAt(int sectorX, int sectorY) {
|
||||
const auto* world = static_cast<const uint8_t*>(World());
|
||||
if (!world || sectorX < 0 || sectorY < 0 || sectorX >= kSectorGrid || sectorY >= kSectorGrid) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint16_t index = reinterpret_cast<const uint16_t*>(world + 0x284)[sectorY * kSectorGrid + sectorX];
|
||||
if (index == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<const uint8_t* const*>(*reinterpret_cast<const uintptr_t*>(world + 0x240))[index];
|
||||
}
|
||||
|
||||
// The sector's explored mask (128 bytes), or null without a sector.
|
||||
inline const uint8_t* SectorExploredMask(int sectorX, int sectorY) {
|
||||
const uint8_t* sector = SectorAt(sectorX, sectorY);
|
||||
return sector ? sector + kSectorExploredMask : nullptr;
|
||||
}
|
||||
|
||||
// Bit of cell (x, y) within its sector's mask (Sector_SetExplored 0x622bf0).
|
||||
inline bool MaskCellExplored(const uint8_t* mask, int cellX, int cellY) {
|
||||
return (mask[cellY * 4 + (cellX >> 3)] >> (cellX & 7)) & 1;
|
||||
}
|
||||
|
||||
// World_SetExplored (0x638f00, thiscall (tile x, tile y, explored), ret 0xc):
|
||||
// sets the bit of the tile's sector; tiles outside the world (>= 0x2000) or in
|
||||
// a sector that isn't loaded are ignored.
|
||||
|
||||
@@ -80,6 +80,22 @@ inline void FillCorners(void* device, float x0, float y0, float x1, float y1, ui
|
||||
quad, 4, 0);
|
||||
}
|
||||
|
||||
// Untextured triangles, three vertices each (a triangle list), in batches the
|
||||
// device takes in one call.
|
||||
inline void FillTriangles(void* device, ScreenVertex* vertices, size_t count) {
|
||||
constexpr DWORD kTriangleList = 4;
|
||||
constexpr size_t kBatch = 3 * 10000;
|
||||
void** vtable = *static_cast<void***>(device);
|
||||
using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*);
|
||||
using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD);
|
||||
reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, nullptr);
|
||||
for (size_t first = 0; first < count; first += kBatch) {
|
||||
const size_t batch = count - first < kBatch ? count - first : kBatch;
|
||||
reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(
|
||||
device, kTriangleList, kFvfScreenColourTexture, vertices + first, static_cast<DWORD>(batch), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Fills (x0, y0)-(x1, y1) with the ARGB colour `color`.
|
||||
inline void FillRect(void* device, float x0, float y0, float x1, float y1, uint32_t color) {
|
||||
FillCorners(device, x0, y0, x1, y1, color, color, color, color);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
// The large map (M, cUI_Megamap): a scrollable view of the 2048x2816 world
|
||||
// map image. docs/hud.md, mods/megamap.cpp.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace game {
|
||||
|
||||
// Scroll position (map pixel at the view's left / top edge).
|
||||
constexpr size_t kMegamapScrollX = 0xc016c;
|
||||
constexpr size_t kMegamapScrollY = 0xc0170;
|
||||
|
||||
inline int32_t MegamapScrollX(const uint8_t* megamap) {
|
||||
return *reinterpret_cast<const int32_t*>(megamap + kMegamapScrollX);
|
||||
}
|
||||
inline int32_t MegamapScrollY(const uint8_t* megamap) {
|
||||
return *reinterpret_cast<const int32_t*>(megamap + kMegamapScrollY);
|
||||
}
|
||||
|
||||
struct Vec3 {
|
||||
float x, y, z;
|
||||
};
|
||||
|
||||
// cUI_Megamap::WorldToMapPixel (0x6cb130, thiscall (out, world x / y / level),
|
||||
// ret 8): the map pixel of a world position (world units, 53.66563 per tile).
|
||||
// Linear: the world plane projection 0x622700, then x * 0.007446 + 1018.75,
|
||||
// y * 0.014914 - 186.95.
|
||||
inline Vec3 WorldToMapPixel(uint8_t* megamap, const Vec3& world) {
|
||||
Vec3 pixel = {};
|
||||
using Fn = void(__fastcall*)(uint8_t*, void*, Vec3*, const Vec3*);
|
||||
reinterpret_cast<Fn>(0x006cb130)(megamap, nullptr, &pixel, &world);
|
||||
return pixel;
|
||||
}
|
||||
|
||||
constexpr float kWorldUnitsPerTile = 53.66563f; // 0x8946b0
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,376 @@
|
||||
// The explored-area border on the large map (M) as a continuous line along
|
||||
// the fog's edge.
|
||||
//
|
||||
// Request (2026-10-26): "the red outline on the mega map is choppy and feels
|
||||
// weird"; replace it, following the explored area (option A).
|
||||
//
|
||||
// Measured (disassembly): cUI_Megamap::DrawVisitedBorder (0x6c8580, called by
|
||||
// the map's render) draws one 2x2 px red dot (0xffff0000) per bit of a 100x100
|
||||
// byte grid (world +0xb73c), at a fixed offset of 10 / 20 px from the sector's
|
||||
// origin towards each neighbour. The grid marks unvisited sectors next to
|
||||
// visited ones (sector flag 0x20000, set when the hero's sector becomes the
|
||||
// centre of the active 3x3, 0x626603; rebuilt on load by 0x633230 and per
|
||||
// sector change by 0x6332d0). So the line was dots in 64-tile steps, and it
|
||||
// followed the sectors stood in, not the explored mask the fog shows (since
|
||||
// the 10x map reveal, far inside the cleared area).
|
||||
//
|
||||
// The mod replaces the draw: the edges between explored and unexplored cells
|
||||
// (2x2 tiles, game/engine/explored.h) of the map's 100x100 sectors are chained
|
||||
// into polylines, simplified to within a pixel and drawn as 2 px lines in the
|
||||
// game's red. The lines are rebuilt while the map is open whenever the masks
|
||||
// changed (checked every 250 ms). Cells of a missing sector count as neither,
|
||||
// so no line runs along the edge of the world.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "game/engine/explored.h"
|
||||
#include "game/ui/draw.h"
|
||||
#include "game/ui/megamap.h"
|
||||
#include "mods/widescreen.h"
|
||||
#include "re/hook.h"
|
||||
#include "re/log.h"
|
||||
|
||||
namespace mods::mapborder {
|
||||
namespace {
|
||||
|
||||
// The map's sector grid (World_RebuildVisitedBorder 0x633230: 100x100).
|
||||
constexpr int kMapSectors = 100;
|
||||
constexpr int kMapCells = kMapSectors * game::kSectorCells;
|
||||
constexpr uint32_t kColour = 0xffff0000; // the game's border colour (0x6c870e)
|
||||
constexpr float kLineWidth = 2.0f; // the game's dots: 2x2 px
|
||||
constexpr double kTolerance = 0.75; // map pixels a simplified line may deviate
|
||||
constexpr DWORD kCheckIntervalMs = 250;
|
||||
constexpr int kScreenHeight = 768;
|
||||
|
||||
enum class Cell : uint8_t { None, Unexplored, Explored };
|
||||
|
||||
struct Grid {
|
||||
const uint8_t* masks[kMapSectors][kMapSectors];
|
||||
|
||||
void Load() {
|
||||
for (int y = 0; y < kMapSectors; ++y) {
|
||||
for (int x = 0; x < kMapSectors; ++x) {
|
||||
masks[y][x] = game::SectorExploredMask(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cell At(int x, int y) const {
|
||||
if (x < 0 || y < 0 || x >= kMapCells || y >= kMapCells) {
|
||||
return Cell::None;
|
||||
}
|
||||
const uint8_t* mask = masks[y / game::kSectorCells][x / game::kSectorCells];
|
||||
if (!mask) {
|
||||
return Cell::None;
|
||||
}
|
||||
return game::MaskCellExplored(mask, x % game::kSectorCells, y % game::kSectorCells) ? Cell::Explored
|
||||
: Cell::Unexplored;
|
||||
}
|
||||
|
||||
// FNV-1a over the masks and which sectors exist.
|
||||
uint64_t Hash() const {
|
||||
uint64_t hash = 0xcbf29ce484222325ull;
|
||||
const auto add = [&](uint8_t byte) { hash = (hash ^ byte) * 0x100000001b3ull; };
|
||||
for (const auto& row : masks) {
|
||||
for (const uint8_t* mask : row) {
|
||||
add(mask != nullptr);
|
||||
for (size_t i = 0; mask && i < 128; ++i) {
|
||||
add(mask[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
struct Point {
|
||||
double x, y;
|
||||
};
|
||||
|
||||
// Cell corner -> map pixel: WorldToMapPixel is linear, so three points give
|
||||
// it for the whole grid.
|
||||
struct Projection {
|
||||
Point origin, stepX, stepY;
|
||||
|
||||
static Point Corner(uint8_t* megamap, int cellX, int cellY) {
|
||||
const float unitsPerCell = game::kExploredCellTiles * game::kWorldUnitsPerTile;
|
||||
const game::Vec3 pixel = game::WorldToMapPixel(megamap, {cellX * unitsPerCell, cellY * unitsPerCell, 0});
|
||||
return {pixel.x, pixel.y};
|
||||
}
|
||||
|
||||
explicit Projection(uint8_t* megamap) {
|
||||
origin = Corner(megamap, 0, 0);
|
||||
const Point right = Corner(megamap, kMapCells, 0);
|
||||
const Point down = Corner(megamap, 0, kMapCells);
|
||||
stepX = {(right.x - origin.x) / kMapCells, (right.y - origin.y) / kMapCells};
|
||||
stepY = {(down.x - origin.x) / kMapCells, (down.y - origin.y) / kMapCells};
|
||||
}
|
||||
|
||||
Point operator()(int cellX, int cellY) const {
|
||||
return {origin.x + cellX * stepX.x + cellY * stepY.x, origin.y + cellX * stepX.y + cellY * stepY.y};
|
||||
}
|
||||
};
|
||||
|
||||
// Corners on the cell grid, (kMapCells + 1)^2 of them.
|
||||
uint32_t Key(int x, int y) {
|
||||
return static_cast<uint32_t>(y) * (kMapCells + 1) + static_cast<uint32_t>(x);
|
||||
}
|
||||
|
||||
struct Edge {
|
||||
int x0, y0, x1, y1;
|
||||
};
|
||||
|
||||
// Every side between an explored and an unexplored cell, clockwise around
|
||||
// the explored cell (screen orientation, y down), so the edges of a border
|
||||
// chain head to tail.
|
||||
std::vector<Edge> BorderEdges(const Grid& grid) {
|
||||
std::vector<Edge> edges;
|
||||
for (int sy = 0; sy < kMapSectors; ++sy) {
|
||||
for (int sx = 0; sx < kMapSectors; ++sx) {
|
||||
const uint8_t* mask = grid.masks[sy][sx];
|
||||
bool any = false;
|
||||
for (int i = 0; mask && i < 128 && !any; ++i) {
|
||||
any = mask[i] != 0;
|
||||
}
|
||||
if (!any) {
|
||||
continue;
|
||||
}
|
||||
for (int ly = 0; ly < game::kSectorCells; ++ly) {
|
||||
for (int lx = 0; lx < game::kSectorCells; ++lx) {
|
||||
if (!game::MaskCellExplored(mask, lx, ly)) {
|
||||
continue;
|
||||
}
|
||||
const int x = sx * game::kSectorCells + lx;
|
||||
const int y = sy * game::kSectorCells + ly;
|
||||
if (grid.At(x, y - 1) == Cell::Unexplored) {
|
||||
edges.push_back({x, y, x + 1, y});
|
||||
}
|
||||
if (grid.At(x + 1, y) == Cell::Unexplored) {
|
||||
edges.push_back({x + 1, y, x + 1, y + 1});
|
||||
}
|
||||
if (grid.At(x, y + 1) == Cell::Unexplored) {
|
||||
edges.push_back({x + 1, y + 1, x, y + 1});
|
||||
}
|
||||
if (grid.At(x - 1, y) == Cell::Unexplored) {
|
||||
edges.push_back({x, y + 1, x, y});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
using Chain = std::vector<std::pair<int, int>>;
|
||||
|
||||
// Edges chained into polylines: open ones (from a corner no edge ends at,
|
||||
// at a missing sector) first, then the closed loops. Where two cells touch
|
||||
// only at a corner, the chain turns right, staying around its own cell.
|
||||
std::vector<Chain> ChainEdges(const std::vector<Edge>& edges) {
|
||||
std::unordered_map<uint32_t, std::vector<size_t>> starting;
|
||||
std::unordered_map<uint32_t, int> ending;
|
||||
starting.reserve(edges.size());
|
||||
ending.reserve(edges.size());
|
||||
for (size_t i = 0; i < edges.size(); ++i) {
|
||||
starting[Key(edges[i].x0, edges[i].y0)].push_back(i);
|
||||
++ending[Key(edges[i].x1, edges[i].y1)];
|
||||
}
|
||||
std::vector<bool> used(edges.size(), false);
|
||||
std::vector<Chain> chains;
|
||||
const auto follow = [&](size_t first) {
|
||||
Chain chain = {{edges[first].x0, edges[first].y0}};
|
||||
size_t current = first;
|
||||
for (;;) {
|
||||
used[current] = true;
|
||||
const Edge& e = edges[current];
|
||||
chain.emplace_back(e.x1, e.y1);
|
||||
const int dx = e.x1 - e.x0;
|
||||
const int dy = e.y1 - e.y0;
|
||||
size_t next = SIZE_MAX;
|
||||
int bestTurn = 3;
|
||||
for (size_t candidate : starting[Key(e.x1, e.y1)]) {
|
||||
if (used[candidate]) {
|
||||
continue;
|
||||
}
|
||||
const int cx = edges[candidate].x1 - edges[candidate].x0;
|
||||
const int cy = edges[candidate].y1 - edges[candidate].y0;
|
||||
// 0 right (-dy, dx), 1 straight, 2 left.
|
||||
const int turn = (cx == -dy && cy == dx) ? 0 : (cx == dx && cy == dy) ? 1 : 2;
|
||||
if (turn < bestTurn) {
|
||||
bestTurn = turn;
|
||||
next = candidate;
|
||||
}
|
||||
}
|
||||
if (next == SIZE_MAX) {
|
||||
break;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
chains.push_back(std::move(chain));
|
||||
};
|
||||
for (size_t i = 0; i < edges.size(); ++i) {
|
||||
if (!used[i] && ending.find(Key(edges[i].x0, edges[i].y0)) == ending.end()) {
|
||||
follow(i);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < edges.size(); ++i) {
|
||||
if (!used[i]) {
|
||||
follow(i);
|
||||
}
|
||||
}
|
||||
return chains;
|
||||
}
|
||||
|
||||
double DistanceToSegment(const Point& p, const Point& a, const Point& b) {
|
||||
const double dx = b.x - a.x;
|
||||
const double dy = b.y - a.y;
|
||||
const double length2 = dx * dx + dy * dy;
|
||||
double t = length2 > 0 ? ((p.x - a.x) * dx + (p.y - a.y) * dy) / length2 : 0;
|
||||
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||
return std::hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
|
||||
}
|
||||
|
||||
// Douglas-Peucker: keeps first and last, and every point needed to stay
|
||||
// within kTolerance of the original.
|
||||
void Simplify(const std::vector<Point>& points, size_t first, size_t last, std::vector<Point>& out) {
|
||||
double farthest = 0;
|
||||
size_t index = first;
|
||||
for (size_t i = first + 1; i < last; ++i) {
|
||||
const double d = DistanceToSegment(points[i], points[first], points[last]);
|
||||
if (d > farthest) {
|
||||
farthest = d;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (farthest > kTolerance) {
|
||||
Simplify(points, first, index, out);
|
||||
Simplify(points, index, last, out);
|
||||
} else {
|
||||
out.push_back(points[last]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Point> SimplifiedLine(const Chain& chain, const Projection& project) {
|
||||
std::vector<Point> points;
|
||||
points.reserve(chain.size());
|
||||
for (const auto& [x, y] : chain) {
|
||||
points.push_back(project(x, y));
|
||||
}
|
||||
std::vector<Point> out = {points.front()};
|
||||
if (points.size() < 2) {
|
||||
return out;
|
||||
}
|
||||
// A closed loop is split at its point farthest from the start, so both
|
||||
// halves have distinct ends.
|
||||
size_t split = points.size() - 1;
|
||||
if (chain.front() == chain.back()) {
|
||||
double farthest = -1;
|
||||
for (size_t i = 1; i + 1 < points.size(); ++i) {
|
||||
const double d = std::hypot(points[i].x - points[0].x, points[i].y - points[0].y);
|
||||
if (d > farthest) {
|
||||
farthest = d;
|
||||
split = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
Simplify(points, 0, split, out);
|
||||
if (split < points.size() - 1) {
|
||||
Simplify(points, split, points.size() - 1, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Map-pixel line segments of the current border.
|
||||
struct Segment {
|
||||
float x0, y0, x1, y1;
|
||||
};
|
||||
std::vector<Segment> g_segments;
|
||||
uint64_t g_hash = 0;
|
||||
DWORD g_lastCheck = 0;
|
||||
bool g_built = false;
|
||||
|
||||
void Rebuild(uint8_t* megamap) {
|
||||
static Grid grid;
|
||||
grid.Load();
|
||||
const uint64_t hash = grid.Hash();
|
||||
if (g_built && hash == g_hash) {
|
||||
return;
|
||||
}
|
||||
const DWORD start = GetTickCount();
|
||||
const Projection project(megamap);
|
||||
const std::vector<Edge> edges = BorderEdges(grid);
|
||||
const std::vector<Chain> chains = ChainEdges(edges);
|
||||
g_segments.clear();
|
||||
for (const Chain& chain : chains) {
|
||||
const std::vector<Point> line = SimplifiedLine(chain, project);
|
||||
for (size_t i = 1; i < line.size(); ++i) {
|
||||
g_segments.push_back({static_cast<float>(line[i - 1].x), static_cast<float>(line[i - 1].y),
|
||||
static_cast<float>(line[i].x), static_cast<float>(line[i].y)});
|
||||
}
|
||||
}
|
||||
if (!g_built) {
|
||||
re::Log("map border: %zu edges, %zu lines, %zu segments, %lu ms", edges.size(), chains.size(),
|
||||
g_segments.size(), GetTickCount() - start);
|
||||
}
|
||||
g_hash = hash;
|
||||
g_built = true;
|
||||
}
|
||||
|
||||
void Draw(uint8_t* megamap, void* device) {
|
||||
const DWORD now = GetTickCount();
|
||||
if (!g_built || now - g_lastCheck >= kCheckIntervalMs) {
|
||||
g_lastCheck = now;
|
||||
Rebuild(megamap);
|
||||
}
|
||||
const float scrollX = static_cast<float>(game::MegamapScrollX(megamap));
|
||||
const float scrollY = static_cast<float>(game::MegamapScrollY(megamap));
|
||||
const float width = static_cast<float>(mods::widescreen::RenderWidth());
|
||||
const float half = kLineWidth / 2;
|
||||
static std::vector<game::ScreenVertex> vertices;
|
||||
vertices.clear();
|
||||
for (const Segment& s : g_segments) {
|
||||
const float x0 = s.x0 - scrollX, y0 = s.y0 - scrollY;
|
||||
const float x1 = s.x1 - scrollX, y1 = s.y1 - scrollY;
|
||||
if ((x0 < -half && x1 < -half) || (x0 > width + half && x1 > width + half) ||
|
||||
(y0 < -half && y1 < -half) || (y0 > kScreenHeight + half && y1 > kScreenHeight + half)) {
|
||||
continue;
|
||||
}
|
||||
const float length = std::hypot(x1 - x0, y1 - y0);
|
||||
if (length <= 0) {
|
||||
continue;
|
||||
}
|
||||
// Along and across the segment, half the width each; the ends reach
|
||||
// half the width further, closing the joints.
|
||||
const float ax = (x1 - x0) / length * half, ay = (y1 - y0) / length * half;
|
||||
const float nx = -ay, ny = ax;
|
||||
const game::ScreenVertex a = {x0 - ax + nx, y0 - ay + ny, 0, 1, kColour, 0, 0, 0};
|
||||
const game::ScreenVertex b = {x0 - ax - nx, y0 - ay - ny, 0, 1, kColour, 0, 0, 0};
|
||||
const game::ScreenVertex c = {x1 + ax + nx, y1 + ay + ny, 0, 1, kColour, 0, 0, 0};
|
||||
const game::ScreenVertex d = {x1 + ax - nx, y1 + ay - ny, 0, 1, kColour, 0, 0, 0};
|
||||
vertices.insert(vertices.end(), {a, b, c, c, b, d});
|
||||
}
|
||||
if (!vertices.empty()) {
|
||||
game::FillTriangles(device, vertices.data(), vertices.size());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mods::mapborder
|
||||
|
||||
namespace game {
|
||||
|
||||
// cUI_Megamap::DrawVisitedBorder (0x6c8580, thiscall (device), ret 4).
|
||||
void __fastcall UiMegamap_DrawVisitedBorder(uint8_t* self, void* edx, void* device);
|
||||
RE_HOOK(0x006c8580, UiMegamap_DrawVisitedBorder);
|
||||
void __fastcall UiMegamap_DrawVisitedBorder(uint8_t* self, void*, void* device) {
|
||||
if (self && device) {
|
||||
mods::mapborder::Draw(self, device);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
+7
-21
@@ -24,6 +24,7 @@
|
||||
#include "game/engine/events.h"
|
||||
#include "game/engine/objects.h"
|
||||
#include "game/engine/orders.h"
|
||||
#include "game/ui/megamap.h"
|
||||
#include "game/ui/windows.h"
|
||||
#include "mods/hud.h"
|
||||
#include "mods/megamap.h"
|
||||
@@ -90,27 +91,12 @@ constexpr uint32_t kZoomedByButton = 0x100;
|
||||
constexpr size_t kZoomHeld = 0xc06bc;
|
||||
constexpr size_t kZoomPointX = 0xc0438;
|
||||
constexpr size_t kZoomPointY = 0xc043c;
|
||||
constexpr size_t kScrollX = 0xc016c;
|
||||
constexpr size_t kScrollY = 0xc0170;
|
||||
|
||||
template <typename T>
|
||||
T& Field(uint8_t* p, size_t offset) {
|
||||
return *reinterpret_cast<T*>(p + offset);
|
||||
}
|
||||
|
||||
// cUI_Megamap 0x6cb130 (thiscall (out x, y, 0, world x, y, level), ret 8): the
|
||||
// map pixel of a world position, as opening the map centres on the hero
|
||||
// (0x6c7bad..).
|
||||
struct Vec3 {
|
||||
float x, y, z;
|
||||
};
|
||||
Vec3 MapPixel(uint8_t* megamap, const Vec3& world) {
|
||||
Vec3 pixel = {};
|
||||
using Fn = void(__fastcall*)(uint8_t*, void*, Vec3*, const Vec3*);
|
||||
reinterpret_cast<Fn>(0x006cb130)(megamap, nullptr, &pixel, &world);
|
||||
return pixel;
|
||||
}
|
||||
|
||||
// Every tick while the zoom button's zoom is on: the zoom point on the hero.
|
||||
void ZoomOnHero(uint8_t*) {
|
||||
uint8_t* megamap = game::UiWindow(game::UiManager(), game::kUiSlotMegamap);
|
||||
@@ -122,12 +108,12 @@ void ZoomOnHero(uint8_t*) {
|
||||
if (!hero) {
|
||||
return;
|
||||
}
|
||||
const Vec3 world = {static_cast<float>(game::CreatureField(hero, game::kCreatureX)),
|
||||
static_cast<float>(game::CreatureField(hero, game::kCreatureY)),
|
||||
static_cast<float>(hero[game::kObjectLevel])};
|
||||
const Vec3 pixel = MapPixel(megamap, world);
|
||||
Field<int32_t>(megamap, kZoomPointX) = static_cast<int32_t>(pixel.x) - Field<int32_t>(megamap, kScrollX);
|
||||
Field<int32_t>(megamap, kZoomPointY) = static_cast<int32_t>(pixel.y) - Field<int32_t>(megamap, kScrollY);
|
||||
const game::Vec3 world = {static_cast<float>(game::CreatureField(hero, game::kCreatureX)),
|
||||
static_cast<float>(game::CreatureField(hero, game::kCreatureY)),
|
||||
static_cast<float>(hero[game::kObjectLevel])};
|
||||
const game::Vec3 pixel = game::WorldToMapPixel(megamap, world);
|
||||
Field<int32_t>(megamap, kZoomPointX) = static_cast<int32_t>(pixel.x) - game::MegamapScrollX(megamap);
|
||||
Field<int32_t>(megamap, kZoomPointY) = static_cast<int32_t>(pixel.y) - game::MegamapScrollY(megamap);
|
||||
}
|
||||
|
||||
struct Registration {
|
||||
|
||||
@@ -431,3 +431,9 @@
|
||||
0x005ff820 cObjectManager::ObjectsInRange # thiscall (out, position, type, only, radius = -1: type sizes), ret 0x14
|
||||
0x00428bb0 TypeTable::ObjectSize # thiscall (type), ret 4: +0x14, default 50
|
||||
0x004fc810 Creature_StunChance # (level, creature): level * 5 / creature level * 100
|
||||
0x00633230 World_RebuildVisitedBorder # thiscall: clears the 100x100 border bytes (+0xb73c) and runs 0x6332d0 for every sector with flag 0x20000 (visited); on load (0x471086, 0x60c621)
|
||||
0x006332d0 World_UpdateVisitedBorderAround # thiscall (sector cx, cy), ret 8: border byte of each unvisited 3x3 neighbour (0x6334f0)
|
||||
0x006334f0 World_ComputeVisitedBorderCell # thiscall (cx, cy), ret 8: 8 bits, one per neighbour sector with flag 0x20000
|
||||
0x006336b0 World_VisitedBorderCell # thiscall (cx, cy), ret 8: border byte, 0 outside 100x100
|
||||
0x006c8580 cUI_Megamap::DrawVisitedBorder # thiscall (device), ret 4: 2x2 px red (0xffff0000) dot per border bit, +-10/+-20 px from the sector origin
|
||||
0x006c87c0 cUI_Megamap::DrawQuestMarkers # thiscall (device), ret 4: quest book entries (0xaaaf00)
|
||||
Reference in new issue
Block a user