Merge pull request #490 from fallout2-ce/feature/mapper-edg

Mapper EDG support
This commit is contained in:
Vlad K
2026-06-08 23:17:25 +02:00
committed by GitHub
22 changed files with 1362 additions and 221 deletions
+4
View File
@@ -614,6 +614,8 @@ if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
target_sources(mapper-ce PUBLIC
${FALLOUT_ENGINE_SOURCES}
${FALLOUT_PLATFORM_SOURCES}
"src/mapper/map_edge_setup.cc"
"src/mapper/map_edge_setup.h"
"src/mapper/map_func.cc"
"src/mapper/map_func.h"
"src/mapper/mapper.cc"
@@ -626,6 +628,8 @@ if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
"src/mapper/mp_scrpt.h"
"src/mapper/mp_targt.cc"
"src/mapper/mp_targt.h"
"src/mapper/mp_utils.cc"
"src/mapper/mp_utils.h"
"src/mapper/mp_text.cc"
"src/mapper/mp_text.h"
)
+2 -6
View File
@@ -2320,12 +2320,8 @@ int _gmouse_3d_move_to(int x, int y, int elevation, Rect* rect)
x1 = -8;
y1 = 13;
if (compat_stricmp(settings.system.executable.c_str(), "mapper") == 0) {
if (tileRoofIsVisible()) {
if ((gDude->flags & OBJECT_HIDDEN) == 0) {
y1 = -83;
}
}
if (settings.system.executableIsMapper() && tileRoofIsVisible() && (gDude->flags & OBJECT_HIDDEN) == 0) {
y1 = -83;
}
} else {
tile = -1;
+69 -40
View File
@@ -29,9 +29,7 @@
#include "map_edge.h"
#include "memory.h"
#include "object.h"
#include "palette.h"
#include "party_member.h"
#include "pipboy.h"
#include "proto.h"
#include "proto_instance.h"
#include "queue.h"
@@ -55,7 +53,7 @@ static int mapLoad(File* stream);
static int _map_age_dead_critters();
static void _map_fix_critter_combat_data();
static int _map_save_file(File* stream);
int _map_save();
int _map_save(bool isInGame);
static void mapMakeMapsDirectory();
static void isoWindowRefreshRect(Rect* rect);
static void isoWindowRefreshRectGame(Rect* rect);
@@ -289,7 +287,7 @@ void isoExit()
// 0x481FB4
void mapInit()
{
if (compat_stricmp(settings.system.executable.c_str(), "mapper") == 0) {
if (settings.system.executableIsMapper()) {
_map_scroll_refresh = isoWindowRefreshRectMapper;
}
@@ -725,6 +723,37 @@ const char* mapBuildPath(const char* name)
return name;
}
const char* mapBuildDataSavePath(const char* relativePath)
{
static char path[COMPAT_MAX_PATH];
// Save root: the validated mapper dev_path when set (mapper only), otherwise the master patches path.
const std::string& devPath = settings.mapper.dev_path;
const char* root = (settings.system.executableIsMapper() && !devPath.empty()) ? devPath.c_str() : settings.system.master_patches_path.c_str();
// Join root + relativePath, tolerating a trailing separator on the root.
size_t rootLen = strlen(root);
bool rootHasSeparator = rootLen > 0 && (root[rootLen - 1] == '\\' || root[rootLen - 1] == '/');
snprintf(path, sizeof(path), "%s%s%s", root, rootHasSeparator ? "" : "\\", relativePath);
// Ensure the directory portion exists.
char dir[COMPAT_MAX_PATH];
snprintf(dir, sizeof(dir), "%s", path);
char* separator = strrchr(dir, '\\');
if (separator != nullptr) {
*separator = '\0';
compat_mkdir_recursive(dir);
}
return path;
}
const char* mapBuildSavePath(const char* name)
{
char relativePath[COMPAT_MAX_PATH];
snprintf(relativePath, sizeof(relativePath), "MAPS\\%s", name);
return mapBuildDataSavePath(relativePath);
}
// 0x482924
int mapSetEnteringLocation(int elevation, int tile_num, int orientation)
{
@@ -761,7 +790,7 @@ void mapNewMap()
tileWindowRefresh();
}
// 0x482A68
// 0x482A68 map_load
int mapLoadByName(char* fileName)
{
int rc;
@@ -770,20 +799,22 @@ int mapLoadByName(char* fileName)
rc = -1;
char* extension = strstr(fileName, ".MAP");
if (extension != nullptr) {
strcpy(extension, ".SAV");
if (!settings.system.executableIsMapper()) {
char* extension = strstr(fileName, ".MAP");
if (extension != nullptr) {
strcpy(extension, ".SAV");
const char* filePath = mapBuildPath(fileName);
const char* filePath = mapBuildPath(fileName);
File* stream = fileOpen(filePath, "rb");
File* stream = fileOpen(filePath, "rb");
strcpy(extension, ".MAP");
strcpy(extension, ".MAP");
if (stream != nullptr) {
fileClose(stream);
rc = mapLoadSaved(fileName);
wmMapMusicStart();
if (stream != nullptr) {
fileClose(stream);
rc = mapLoadSaved(fileName);
wmMapMusicStart();
}
}
}
@@ -823,15 +854,17 @@ int mapLoadById(int map)
return rc;
}
// 0x482B74
// 0x482B74 map_load_file
static int mapLoad(File* stream)
{
_map_save_in_game(true);
int mapLoadSoundId = 0;
if (backgoundSoundIsPlaying()) {
// Use the sfall sound path so the map-loading ambience does not depend
// on the native background music loader.
mapLoadSoundId = scriptSoundPlay("sound\\music\\WIND2.ACM", SCRIPT_SOUND_MODE_LOOP);
if (!settings.system.executableIsMapper()) {
_map_save_in_game(true);
if (backgoundSoundIsPlaying()) {
// Use the sfall sound path so the map-loading ambience does not depend
// on the native background music loader.
mapLoadSoundId = scriptSoundPlay("sound\\music\\WIND2.ACM", SCRIPT_SOUND_MODE_LOOP);
}
}
isoDisable();
_partyMemberPrepLoad();
@@ -935,7 +968,7 @@ static int mapLoad(File* stream)
goto err;
}
if (settings.ui.edg_support && !settings.ui.ignore_map_edges) {
if (settings.system.executableIsMapper() || settings.ui.edg_support) {
mapEdgeLoad(gMapHeader.name);
}
@@ -1083,7 +1116,7 @@ err:
return rc;
}
// 0x483188
// 0x483188 map_load_in_game
int mapLoadSaved(char* fileName)
{
debugPrint("\nMAP: Loading SAVED map.");
@@ -1343,32 +1376,26 @@ static void _map_fix_critter_combat_data()
// map_save
// 0x483850
int _map_save()
int _map_save(bool isInGame)
{
char temp[80];
temp[0] = '\0';
strcat(temp, settings.system.master_patches_path.c_str());
compat_mkdir(temp);
strcat(temp, "\\MAPS");
compat_mkdir(temp);
int rc = -1;
if (gMapHeader.name[0] != '\0') {
const char* mapFileName = mapBuildPath(gMapHeader.name);
File* stream = fileOpen(mapFileName, "wb");
const char* mapFilePath = mapBuildSavePath(gMapHeader.name);
File* stream = fileOpen(mapFilePath, "wb");
if (stream != nullptr) {
rc = _map_save_file(stream);
fileClose(stream);
} else {
snprintf(temp, sizeof(temp), "Unable to open %s to write!", gMapHeader.name);
debugPrint(temp);
debugPrint("Unable to open %s to write!", gMapHeader.name);
}
if (rc == 0) {
snprintf(temp, sizeof(temp), "%s saved.", gMapHeader.name);
debugPrint(temp);
debugPrint("%s saved.", gMapHeader.name);
if (!isInGame) {
// Write the edge (.EDG) alongside the map.
mapEdgeSave(gMapHeader.name);
}
}
} else {
debugPrint("\nError: map_save: map header corrupt!");
@@ -1502,7 +1529,7 @@ int _map_save_in_game(bool isLeavingMap)
strcpy(name, gMapHeader.name);
_strmfe(gMapHeader.name, name, "SAV");
if (_map_save() == -1) {
if (_map_save(true) == -1) {
return -1;
}
@@ -1603,6 +1630,8 @@ static void isoWindowRefreshRectMapper(Rect* rect)
if (!hasVisArea) {
tile_hires_stencil_draw(&rectToUpdate, gIsoWindowBuffer, rectGetWidth(&gIsoWindowRect), rectGetHeight(&gIsoWindowRect));
}
tileMapperOverlayRender(gIsoWindowBuffer, rectGetWidth(&gIsoWindowRect), gElevation, &rectToUpdate);
}
// NOTE: Inlined.
+7 -1
View File
@@ -106,12 +106,18 @@ void mapNewMap();
int mapLoadByName(char* fileName);
int mapLoadById(int map_index);
const char* mapBuildPath(const char* name);
// Resolves a VFS-relative data path (e.g. "MAPS\\ARROYO.MAP") to a writable path, creating its directory.
// Save root is the validated mapper dev_path when set, otherwise the master patches path.
const char* mapBuildDataSavePath(const char* relativePath);
// Convenience wrapper around mapBuildDataSavePath for files under MAPS\, mirroring
// mapBuildPath semantics: name is the bare filename, e.g. "ARROYO.MAP".
const char* mapBuildSavePath(const char* name);
int mapLoadSaved(char* fileName);
int mapGetLoadedAreaId();
int mapSetTransition(MapTransition* transition);
int mapHandleTransition();
int _map_save_in_game(bool isLeavingMap);
int _map_save();
int _map_save(bool isInGame = false);
} // namespace fallout
+165 -65
View File
@@ -6,12 +6,12 @@
#include "map.h"
#include "map_defs.h"
#include "platform_compat.h"
#include "settings.h"
#include "svga.h"
#include "tile.h"
#include "window_manager.h"
#include <cassert>
#include <memory>
namespace fallout {
@@ -19,8 +19,9 @@ namespace fallout {
constexpr int kTileWidth = 32;
constexpr int kTileHeight = 24;
static std::unique_ptr<EdgeZone> edgeZones[ELEVATION_COUNT];
static EdgeElevationData edgeData[ELEVATION_COUNT];
static bool edgeDataLoaded = false;
static bool mapperMode = false;
static bool edgeVersion2 = false;
static EdgeZone* currentEdgeZone = nullptr;
@@ -152,30 +153,62 @@ static void calcEdgeData(EdgeZone* zone)
// none contains it.
static EdgeZone* findZoneByPixel(int px, int py, int elevation)
{
EdgeZone* zone = edgeZones[elevation].get();
if (zone == nullptr) {
std::vector<EdgeZone>& zones = edgeData[elevation].zones;
if (zones.empty()) {
return nullptr;
}
// Multi-edge: advance while target is outside current zone.
// Multi-edge: advance while target is outside current zone, stopping at the last.
// width/height in original are half-window values (winHalfWidth-1, winHalfHeight+1),
// so window size cancels out of the condition — only pixelRect ± small constants remain.
constexpr int kZoneMarginY = 2;
while (zone->next != nullptr) {
size_t index = 0;
while (index + 1 < zones.size()) {
const EdgeZone& zone = zones[index];
// Point is inside current zone.
if (px < zone->pixelRect.left && px > zone->pixelRect.right
&& py > zone->pixelRect.top - kZoneMarginY && py < zone->pixelRect.bottom - kZoneMarginY) {
if (px < zone.pixelRect.left && px > zone.pixelRect.right
&& py > zone.pixelRect.top - kZoneMarginY && py < zone.pixelRect.bottom - kZoneMarginY) {
break;
}
zone = zone->next.get();
index++;
}
return zone;
return &zones[index];
}
// Load EDG file, populate edgeZones, and compute pixel-space fields.
// EDG files use big-endian byte order (like all Fallout 2 files).
// Build the "<name>.EDG" name.
static void buildEdgeFileName(const char* mapName, char* outPath, size_t outSize)
{
char fname[COMPAT_MAX_FNAME];
compat_splitpath(mapName, nullptr, nullptr, fname, nullptr);
snprintf(outPath, outSize, "%s.EDG", fname);
}
// Unpacks the EDG clip-sides bitfield (per-elevation in v2).
static EdgeZone::ClipSides unpackClipSides(int raw)
{
EdgeZone::ClipSides clip;
clip.bottom = (raw & 1) != 0;
clip.right = ((raw >> 8) & 1) != 0;
clip.top = ((raw >> 16) & 1) != 0;
clip.left = ((raw >> 24) & 1) != 0;
return clip;
}
// Packs clip-sides into the EDG bitfield (inverse of unpackClipSides).
static int packClipSides(const EdgeZone::ClipSides& clip)
{
int value = 0;
if (clip.bottom) value |= 1;
if (clip.right) value |= 1 << 8;
if (clip.top) value |= 1 << 16;
if (clip.left) value |= 1 << 24;
return value;
}
// Parse an EDG stream into edgeData / edgeVersion2 (big-endian byte order), computing
// the runtime pixel-space fields for each zone.
static bool mapEdgeLoadFromStream(File* stream)
{
int magic;
@@ -192,57 +225,35 @@ static bool mapEdgeLoadFromStream(File* stream)
int levelIndicator = 0;
for (int elev = 0; elev < ELEVATION_COUNT; elev++) {
int sqLeft = SQUARE_GRID_WIDTH - 1, sqTop = 0, sqRight = 0, sqBottom = SQUARE_GRID_HEIGHT - 1;
int sqClipData = 0;
EdgeElevationData& data = edgeData[elev];
data.squareRect = { SQUARE_GRID_WIDTH - 1, 0, 0, SQUARE_GRID_HEIGHT - 1 };
data.clipSides = {};
if (edgeVersion2) {
int sqRect[4];
if (fileReadInt32List(stream, sqRect, 4) == -1
|| fileReadInt32(stream, &sqClipData) == -1) {
int sqClipData;
if (fileReadInt32List(stream, sqRect, 4) == -1 || fileReadInt32(stream, &sqClipData) == -1) {
return false;
}
sqLeft = sqRect[0];
sqTop = sqRect[1];
sqRight = sqRect[2];
sqBottom = sqRect[3];
data.squareRect = { sqRect[0], sqRect[1], sqRect[2], sqRect[3] };
data.clipSides = unpackClipSides(sqClipData);
}
if (levelIndicator != elev) {
continue; // no tileRect data for this elevation
}
auto tail = &edgeZones[elev];
bool isFirstZone = true;
while (true) {
int tileRect[4];
if (fileReadInt32List(stream, tileRect, 4) == -1) {
return elev == ELEVATION_COUNT - 1;
}
auto zone = std::make_unique<EdgeZone>();
// File stores RECT order: [0]=left, [1]=top, [2]=right, [3]=bottom.
zone->tileRect.left = tileRect[0];
zone->tileRect.top = tileRect[1];
zone->tileRect.right = tileRect[2];
zone->tileRect.bottom = tileRect[3];
zone->squareRect.left = sqLeft;
zone->squareRect.top = sqTop;
zone->squareRect.right = sqRight;
zone->squareRect.bottom = sqBottom;
int rawClip = isFirstZone ? sqClipData : 0;
zone->clipSides.bottom = (rawClip & 1) != 0;
zone->clipSides.right = ((rawClip >> 8) & 1) != 0;
zone->clipSides.top = ((rawClip >> 16) & 1) != 0;
zone->clipSides.left = ((rawClip >> 24) & 1) != 0;
zone->next = nullptr;
calcEdgeData(zone.get());
*tail = std::move(zone);
tail = &(*tail)->next;
isFirstZone = false;
EdgeZone zone {};
zone.tileRect = { tileRect[0], tileRect[1], tileRect[2], tileRect[3] };
calcEdgeData(&zone);
data.zones.push_back(zone);
if (fileReadInt32(stream, &levelIndicator) == -1) {
return elev == ELEVATION_COUNT - 1;
@@ -261,11 +272,9 @@ void mapEdgeLoad(const char* mapName)
{
mapEdgeFree();
char fname[COMPAT_MAX_FNAME];
compat_splitpath(mapName, nullptr, nullptr, fname, nullptr);
char edgPath[COMPAT_MAX_PATH];
snprintf(edgPath, sizeof(edgPath), "MAPS\\%s.EDG", fname);
char edgName[COMPAT_MAX_PATH];
buildEdgeFileName(mapName, edgName, sizeof(edgName));
const char* edgPath = mapBuildPath(edgName);
File* stream = fileOpen(edgPath, "rb");
if (stream == nullptr) {
@@ -284,10 +293,84 @@ void mapEdgeLoad(const char* mapName)
}
}
// Index of the next elevation (>= from) that has zones, or ELEVATION_COUNT if none.
// Used as the level indicator that advances the loader past empty elevations.
static int nextElevationWithZones(int from)
{
for (int elev = from; elev < ELEVATION_COUNT; elev++) {
if (!edgeData[elev].zones.empty()) {
return elev;
}
}
return ELEVATION_COUNT;
}
// Writes edgeData to an EDG stream, mirroring mapEdgeLoadFromStream's byte layout.
static bool writeEdgStream(File* stream)
{
if (fileWriteInt32(stream, 'EDGE') == -1) return false;
if (fileWriteInt32(stream, edgeVersion2 ? 2 : 1) == -1) return false;
if (fileWriteInt32(stream, 0) == -1) return false; // reserved
for (int elev = 0; elev < ELEVATION_COUNT; elev++) {
const EdgeElevationData& data = edgeData[elev];
if (edgeVersion2) {
int sqRect[4] = { data.squareRect.left, data.squareRect.top, data.squareRect.right, data.squareRect.bottom };
if (fileWriteInt32List(stream, sqRect, 4) == -1) return false;
if (fileWriteInt32(stream, packClipSides(data.clipSides)) == -1) return false;
}
const int zoneCount = static_cast<int>(data.zones.size());
for (int i = 0; i < zoneCount; i++) {
const Rect& r = data.zones[i].tileRect;
int tileRect[4] = { r.left, r.top, r.right, r.bottom };
if (fileWriteInt32List(stream, tileRect, 4) == -1) return false;
// Level indicator: same elevation while more zones follow, otherwise the
// index of the next elevation that has zones (so the loader advances to it).
int levelIndicator = (i + 1 < zoneCount) ? elev : nextElevationWithZones(elev + 1);
if (fileWriteInt32(stream, levelIndicator) == -1) return false;
}
}
return true;
}
void mapEdgeSave(const char* mapName)
{
int totalZones = 0;
for (const EdgeElevationData& data : edgeData) {
totalZones += static_cast<int>(data.zones.size());
}
if (totalZones == 0) {
return; // nothing to write
}
char edgName[COMPAT_MAX_PATH];
buildEdgeFileName(mapName, edgName, sizeof(edgName));
const char* edgPath = mapBuildSavePath(edgName);
File* stream = fileOpen(edgPath, "wb");
if (stream == nullptr) {
debugPrint("mapEdgeSave: unable to open %s for writing\n", edgPath);
return;
}
bool ok = writeEdgStream(stream);
fileClose(stream);
debugPrint("mapEdgeSave: %s %s\n", ok ? "wrote" : "error writing", edgPath);
}
EdgeElevationData& mapEdgeGetElevationData(int elevation)
{
return edgeData[elevation];
}
void mapEdgeFree()
{
for (auto& gEdgeZone : edgeZones) {
gEdgeZone.reset();
for (auto& data : edgeData) {
data = EdgeElevationData {};
}
edgeDataLoaded = false;
currentEdgeZone = nullptr;
@@ -303,6 +386,27 @@ bool mapEdgeIsLoaded()
return edgeDataLoaded;
}
void mapEdgeSetMapperMode(bool enabled)
{
mapperMode = enabled;
}
bool mapEdgeIsMapperMode()
{
return mapperMode;
}
bool mapEdgeIsEnabled()
{
// Enforced when data is loaded, we're not editing in the mapper, and the user enabled it.
return edgeDataLoaded && !mapperMode && settings.ui.edg_support && !settings.ui.ignore_map_edges;
}
void mapEdgeUpgradeToVersion2()
{
edgeVersion2 = true;
}
bool mapEdgeZoneIsSelected()
{
return currentEdgeZone != nullptr;
@@ -393,36 +497,32 @@ int mapEdgeGetTileYAlignment() { return currentTileYAlignment; }
bool mapEdgeHasSquareRect(int elevation)
{
const auto& zone = edgeZones[elevation];
return edgeVersion2 && zone != nullptr && zone->squareRect.left >= 0;
const EdgeElevationData& data = edgeData[elevation];
return edgeVersion2 && !data.zones.empty() && data.squareRect.left >= 0;
}
void mapEdgeGetSquareRect(int elevation, Rect* outRect)
{
const auto& zone = edgeZones[elevation];
*outRect = zone->squareRect;
*outRect = edgeData[elevation].squareRect;
}
EdgeZone::ClipSides mapEdgeGetClipSides(int elevation)
{
const auto& zone = edgeZones[elevation];
return zone ? zone->clipSides : EdgeZone::ClipSides {};
return edgeData[elevation].clipSides;
}
void mapEdgeRecalc()
{
for (auto& gEdgeZone : edgeZones) {
const auto* zone = &gEdgeZone;
while (zone != nullptr) {
calcEdgeData(zone->get());
zone = &zone->get()->next;
for (auto& data : edgeData) {
for (auto& zone : data.zones) {
calcEdgeData(&zone);
}
}
}
bool mapEdgeComputeVisibleArea(int elevation, Rect* outRect)
{
if (!edgeDataLoaded) return false;
if (!mapEdgeIsEnabled()) return false;
int px, py;
tileToPixelOffset(gCenterTile, px, py);
@@ -453,7 +553,7 @@ bool mapEdgeComputeVisibleArea(int elevation, Rect* outRect)
bool mapEdgeIsOverClippedArea(int screenX, int screenY)
{
if (!edgeDataLoaded) return false;
if (!mapEdgeIsEnabled()) return false;
if (screenX >= gMapVisibleArea.left && screenX <= gMapVisibleArea.right && screenY >= gMapVisibleArea.top && screenY < gMapVisibleArea.bottom) return false;
+30 -6
View File
@@ -1,7 +1,7 @@
#ifndef MAP_EDGE_H
#define MAP_EDGE_H
#include <memory>
#include <vector>
#include "geometry.h"
@@ -19,6 +19,7 @@ struct EdgeZone {
Rect tileRect;
// Pixel-space rect from tileRect corner conversion (before contraction).
// Runtime-calculated by calcEdgeData(); stale while the editor mutates tileRect.
Rect pixelRect;
// Pixel-space boundary for center-tile scroll blocking (screen-size dependent).
@@ -26,28 +27,51 @@ struct EdgeZone {
// X axis is inverted: left > right (smaller tile index → larger pixel X).
// Y axis is normal: bottom > top.
Rect scrollBorderRect;
};
// Square-grid clip rect for floor/roof rendering (v2 EDG only).
// Valid when left >= 0.
// All edge data for a single elevation. squareRect/clipSides are per-elevation (v2 EDG),
// matching the file format; zones is the list of edge rects (one per zone).
struct EdgeElevationData {
std::vector<EdgeZone> zones;
// Square-grid clip rect for floor/roof rendering (v2 EDG only). Valid when left >= 0.
Rect squareRect;
// Per-side clip flags unpacked from EDG v2. True means the black square overlay
// for that side is drawn on top of (after) non-flat objects.
ClipSides clipSides;
std::unique_ptr<EdgeZone> next;
EdgeZone::ClipSides clipSides;
};
// Load EDG file for a map. mapName is the raw map filename e.g. "ARROYO.MAP".
// Silently does nothing if no .edg file is found.
void mapEdgeLoad(const char* mapName);
// Writes the current in-memory edge data to the map's EDG file (mapper save path).
// Does nothing when there are no edge zones. Used by the mapper map-save flow.
void mapEdgeSave(const char* mapName);
// Mutable access to a single elevation's edge data. Used by the mapper edge editor,
// which edits this data in place; the disk write happens later via mapEdgeSave.
EdgeElevationData& mapEdgeGetElevationData(int elevation);
// Free all loaded EDG data. Safe to call when nothing is loaded.
void mapEdgeFree();
// Returns true if EDG data was successfully loaded for the current map.
bool mapEdgeIsLoaded();
// Mapper-editing mode: when enabled, loaded EDG data is not enforced so edges can be edited.
// The game leaves this off; the mapper turns it off only while play-testing.
void mapEdgeSetMapperMode(bool enabled);
bool mapEdgeIsMapperMode();
// True when edge constraints are actively enforced: data loaded, not in mapper-editing mode,
// and enabled by user settings (edg_support on, ignore_map_edges off).
bool mapEdgeIsEnabled();
// Marks edge data as version 2 so squareRect/clipSides are written on save.
void mapEdgeUpgradeToVersion2();
// Returns true if a zone was selected on last tileSetCenter call.
bool mapEdgeZoneIsSelected();
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
#ifndef MAPPER_MAP_EDGE_SETUP_H
#define MAPPER_MAP_EDGE_SETUP_H
namespace fallout {
// Registers the iso-view overlay hook with the tile renderer. Call once at mapper init.
void mapEdgeSetupInit();
// Opens the Map Edge Setup dialog to edit the current map's edge rects.
// Edits the in-memory edge data; the disk write happens on the next map save.
void mapEdgeSetupDialog();
// Opens the Map Angled Edge Setup dialog to edit the current elevation's squareRect/clipSides.
// Edits the in-memory edge data; the disk write happens on the next map save.
void mapEdgeSquareSetupDialog();
// Toggles the persistent edge-rects overlay shown over the iso view while editing.
void mapEdgeSetupToggleOverlay();
} // namespace fallout
#endif /* MAPPER_MAP_EDGE_SETUP_H */
+1
View File
@@ -18,6 +18,7 @@
#include "kb.h"
#include "map.h"
#include "mapper/mapper.h"
#include "mapper/mp_utils.h"
#include "memory.h"
#include "mouse.h"
#include "object.h"
+105 -67
View File
@@ -1,5 +1,7 @@
#include "mapper/mapper.h"
#include "mapper/mp_utils.h"
#include <algorithm>
#include <ctype.h>
#include <stdio.h>
@@ -30,6 +32,8 @@
#include "light.h"
#include "loadsave.h"
#include "map.h"
#include "map_edge.h"
#include "mapper/map_edge_setup.h"
#include "mapper/map_func.h"
#include "mapper/mp_instance.h"
#include "mapper/mp_proto.h"
@@ -50,6 +54,7 @@
#include "window_manager.h"
#include "window_manager_private.h"
#include "worldmap.h"
#include "xfile.h"
namespace fallout {
@@ -133,6 +138,10 @@ static char kRebuildBinary[] = " Rebuild Binary ";
static char kArtToProtos[] = " Art => New Protos ";
static char kSwapPrototypse[] = " Swap Prototypes ";
static char kHiResMapEdges[] = " Hi-Res Map Edges Setup ";
static char kSetAngledEdges[] = " Set Hi-Res Angled Edges ";
static char kToggleMapEdgesOverlay[] = " Toggle Map Edges Overlay ";
static char kTmpMapName[] = "TMP$MAP#.MAP";
// Stored statically in CE so it survives across enter/exit.
@@ -218,12 +227,19 @@ char* menu_3[] = {
kSwapPrototypse,
};
char* menuNamesSettings[] = {
kHiResMapEdges,
kSetAngledEdges,
kToggleMapEdgesOverlay,
};
// 0x5596F8
char** menu_names[] = {
menu_0,
menu_1,
menu_2,
menu_3,
menuNamesSettings,
};
// 0x559748
@@ -283,6 +299,8 @@ int menu_val_2[8];
int menu_val_3[7];
int menu_val_4[3];
// 0x6EAA80
unsigned char e_num[4][19 * 26];
@@ -544,6 +562,7 @@ constexpr int kBtnMenuHeaderFile = KEY_ALT_F;
constexpr int kBtnMenuHeaderTools = KEY_ALT_V;
constexpr int kBtnMenuHeaderScripts = KEY_ALT_T;
constexpr int kBtnMenuHeaderLibrarian = KEY_ALT_J;
constexpr int kBtnMenuHeaderSettings = KEY_ALT_X;
constexpr int kBtnPlayMode = KEY_F8;
constexpr int kBtnRebuildProtoList = KEY_F10;
@@ -559,6 +578,9 @@ constexpr int kBtnMarkAllExitGrids = 5679;
constexpr int kBtnClearMapLevel = 5666;
constexpr int kBtnCreateAllMapTexts = 5406;
constexpr int kBtnRebuildAllMaps = 5405;
constexpr int kBtnHiResMapEdges = 0x3101;
constexpr int kBtnSetAngledEdges = 0x3103;
constexpr int kBtnToggleMapEdgesOverlay = 0x3102;
// FILE menu pulldown keycodes
constexpr int kBtnNew = KEY_ALT_N;
@@ -717,6 +739,10 @@ void MapperInit()
menu_val_3[4] = KEY_ESCAPE;
menu_val_3[5] = kBtnLibrarianArtToProtos;
menu_val_3[6] = kBtnLibrarianSwapProtos;
menu_val_4[0] = kBtnHiResMapEdges;
menu_val_4[1] = kBtnSetAngledEdges;
menu_val_4[2] = kBtnToggleMapEdgesOverlay;
}
static int loadMapperLbm(int lbmBufWidth, int lbmBufHeight)
@@ -730,6 +756,42 @@ static int loadMapperLbm(int lbmBufWidth, int lbmBufHeight)
lbmBufHeight - 1);
}
// Validates the configurable mapper dev_path: it must be an existing folder mounted in the
// VFS. When invalid, it is cleared (so saves fall back to the patches path) and the user is warned.
static void mapperValidateDevPath()
{
std::string& devPath = settings.mapper.dev_path;
if (devPath.empty()) {
return;
}
if (compat_is_dir(devPath.c_str()) && xbaseIsValidDirectory(devPath.c_str())) {
return;
}
char msg[COMPAT_MAX_PATH + 128];
snprintf(msg, sizeof(msg), "Mapper dev_path \"%s\" is not a valid mounted data folder. Ignoring it.", devPath.c_str());
showMessageBox(msg);
devPath.clear();
}
static void initMenuBar(const int screenWidth)
{
int foregroundColor = _colorTable[20052];
int backgroundColor = _colorTable[8456];
menu_bar = windowCreate(0, 0, screenWidth, 16, _colorTable[0], WINDOW_HIDDEN);
_win_register_menu_bar(menu_bar, 0, 0, screenWidth, 16, foregroundColor, backgroundColor);
_win_register_menu_pulldown(menu_bar, 8, "FILE", kBtnMenuHeaderFile, 8, menu_names[0], foregroundColor, backgroundColor);
_win_register_menu_pulldown(menu_bar, 40, "TOOLS", kBtnMenuHeaderTools, 21, menu_names[1], foregroundColor, backgroundColor);
_win_register_menu_pulldown(menu_bar, 80, "SCRIPTS", kBtnMenuHeaderScripts, 8, menu_names[2], foregroundColor, backgroundColor);
_win_register_menu_pulldown(menu_bar, 130, "SETTINGS", kBtnMenuHeaderSettings, 3, menu_names[4], foregroundColor, backgroundColor);
if (can_modify_protos) {
_win_register_menu_pulldown(menu_bar, 180, "LIBRARIAN", kBtnMenuHeaderLibrarian, 7, menu_names[3], foregroundColor, backgroundColor);
}
}
// 0x485F94
int mapper_edit_init(int argc, char** argv)
{
@@ -768,54 +830,7 @@ int mapper_edit_init(int argc, char** argv)
max_art_buttons = (screenWidth - 135) / 50;
menu_bar = windowCreate(0,
0,
screenWidth,
16,
_colorTable[0],
WINDOW_HIDDEN);
_win_register_menu_bar(menu_bar,
0,
0,
screenWidth,
16,
260,
_colorTable[8456]);
_win_register_menu_pulldown(menu_bar,
8,
"FILE",
kBtnMenuHeaderFile,
8,
menu_names[0],
260,
_colorTable[8456]);
_win_register_menu_pulldown(menu_bar,
40,
"TOOLS",
kBtnMenuHeaderTools,
21,
menu_names[1],
260,
_colorTable[8456]);
_win_register_menu_pulldown(menu_bar,
80,
"SCRIPTS",
kBtnMenuHeaderScripts,
8,
menu_names[2],
260,
_colorTable[8456]);
if (can_modify_protos) {
_win_register_menu_pulldown(menu_bar,
130,
"LIBRARIAN",
kBtnMenuHeaderLibrarian,
7,
menu_names[3],
260,
_colorTable[8456]);
}
initMenuBar(screenWidth);
tool_win = windowCreate(0,
_scr_size.bottom - 99,
@@ -995,6 +1010,9 @@ int mapper_edit_init(int argc, char** argv)
mapInit();
target_init();
mouseShowCursor();
mapperValidateDevPath();
mapEdgeSetupInit();
mapEdgeSetMapperMode(true);
if (settings.mapper.rebuild_protos) {
proto_build_all_texts();
@@ -1007,10 +1025,7 @@ int mapper_edit_init(int argc, char** argv)
// 0x48752C
void mapper_edit_exit()
{
remove(mapBuildPath("TMP$MAP#.MAP"));
remove(mapBuildPath("TMP$MAP#.CFG"));
MapDirErase("MAPS\\", "SAV");
mapper_remove_tmp_map_files();
if (can_modify_protos) {
copy_proto_lists();
@@ -1314,7 +1329,7 @@ void edit_mapper()
int tile = gGameMouseBouncingCursor->tile;
if (settings.debug.show_tile_num) {
debugPrint("tilenum = %d ", tile);
debugPrint("\ntilenum = %d ", tile);
}
// Display tile number on toolbar (vanilla: x=7, y=27, maxWidth=260, color=35)
char tileNumStr[32];
@@ -1413,6 +1428,10 @@ void edit_mapper()
int index = inputGetInput();
if (index == -1) continue;
keyCode = menu_val_3[index];
} else if (keyCode == kBtnMenuHeaderSettings) {
int index = inputGetInput();
if (index == -1) continue;
keyCode = menu_val_4[index];
}
// Toolbar art-slot left-click: select proto from toolbar
@@ -1485,7 +1504,8 @@ void edit_mapper()
if (mapperYesNoDialog("Erase this map?")) {
bool wasBlockOn = map_toggle_block_obj_viewing_on();
if (wasBlockOn) map_toggle_block_obj_viewing(0);
mapper_destroy_highlight_obj(&hl_obj1, &_screen_obj);
mapper_destroy_highlight_obj(&hl_obj1, nullptr);
_screen_obj = nullptr;
mapNewMap();
handle_new_map(&currentType, &scrollOffset);
interfaceBarHide();
@@ -1501,7 +1521,8 @@ void edit_mapper()
if (settings.mapper.use_art_not_protos) {
mapperShowTimedMsg("WARNING! You are loading ART, not PROTOS!!!");
}
mapper_destroy_highlight_obj(&hl_obj1, &_screen_obj);
mapper_destroy_highlight_obj(&hl_obj1, nullptr);
_screen_obj = nullptr;
bool wasBlockOn = map_toggle_block_obj_viewing_on();
if (wasBlockOn) {
map_toggle_block_obj_viewing(0);
@@ -1550,6 +1571,23 @@ void edit_mapper()
case kBtnInfo:
// No op, matching original.
break;
case kBtnHiResMapEdges:
if (map_entered) {
mapperShowTimedMsg("This map has been Entered. Can't edit edges.");
break;
}
mapEdgeSetupDialog();
break;
case kBtnSetAngledEdges:
if (map_entered) {
mapperShowTimedMsg("This map has been Entered. Can't edit edges.");
break;
}
mapEdgeSquareSetupDialog();
break;
case kBtnToggleMapEdgesOverlay:
mapEdgeSetupToggleOverlay();
break;
case kBtnSaveAs: {
if (map_entered) {
mapperShowTimedMsg("This map has been Entered. Can't Save.");
@@ -2675,8 +2713,12 @@ int mapper_mark_exit_grid()
// and on editor shutdown if the user quit while still in play mode.
static void mapper_remove_tmp_map_files()
{
remove(mapBuildPath(tmp_map_name));
remove(mapBuildPath("TMP$MAP#.CFG"));
remove(mapBuildSavePath(kTmpMapName));
remove(mapBuildSavePath("TMP$MAP#.EDG"));
char cfgPath[COMPAT_MAX_PATH];
snprintf(cfgPath, sizeof(cfgPath), "%s\\MAPS\\TMP$MAP#.CFG", settings.system.master_patches_path.c_str());
remove(cfgPath);
MapDirErase("MAPS\\", "SAV");
}
@@ -2742,6 +2784,9 @@ static void mapper_enter_play_mode(Object** pHlObj1)
tileScrollBlockingEnable();
tileScrollLimitingEnable();
// Leave mapper-editing mode so loaded edges behave as in the game (subject to settings).
mapEdgeSetMapperMode(false);
if (settings.mapper.run_mapper_as_game) {
scriptExecProc(gMapSid, SCRIPT_PROC_MAP_ENTER);
if (scriptsExecStartProc() == -1) {
@@ -2805,6 +2850,9 @@ static void mapper_exit_play_mode(int* pCurrentType, int* pScrollOffset, Object*
tileScrollBlockingDisable();
tileScrollLimitingDisable();
// Back to editing: loaded edges are kept but no longer enforced.
mapEdgeSetMapperMode(true);
// Match the original: if click-to-scroll was on during play, force it off on exit.
if (_gmouse_get_click_to_scroll()) {
_gmouse_set_click_to_scroll(false);
@@ -2835,14 +2883,4 @@ static void mapper_mark_all_exit_grids()
}
}
void mapperShowTimedMsg(const char* msg)
{
win_timed_msg(msg, _colorTable[31744] | FONT_SHADOW);
}
bool mapperYesNoDialog(const char* msg)
{
return win_yes_no(msg, 80, 80, 0x104 | FONT_SHADOW) > 0;
}
} // namespace fallout
-3
View File
@@ -18,9 +18,6 @@ extern unsigned char e_num[4][19 * 26];
int mapper_main(int argc, char** argv);
void print_toolbar_name(int object_type);
int mapper_inven_unwield(Object* obj, int right_hand);
void mapperShowTimedMsg(const char* msg);
bool mapperYesNoDialog(const char* msg);
} // namespace fallout
#endif /* FALLOUT_MAPPER_MAPPER_H_ */
+1 -1
View File
@@ -3,8 +3,8 @@
#include "color.h"
#include "input.h"
#include "kb.h"
#include "mapper/mapper.h"
#include "mapper/mp_proto.h"
#include "mapper/mp_utils.h"
#include "obj_types.h"
#include "window_manager_private.h"
+24
View File
@@ -0,0 +1,24 @@
#include "mapper/mp_utils.h"
#include "color.h"
#include "text_font.h"
#include "window_manager_private.h"
namespace fallout {
void mapperShowTimedMsg(const char* msg)
{
win_timed_msg(msg, _colorTable[31744] | FONT_SHADOW);
}
bool mapperYesNoDialog(const char* msg)
{
return win_yes_no(msg, 80, 80, 0x104 | FONT_SHADOW) > 0;
}
void mapperShowMessage(const char* msg)
{
_win_msg(msg, 80, 80, _colorTable[31744] | FONT_SHADOW);
}
} // namespace fallout
+12
View File
@@ -0,0 +1,12 @@
#ifndef FALLOUT_MAPPER_MP_UTILS_H_
#define FALLOUT_MAPPER_MP_UTILS_H_
namespace fallout {
void mapperShowTimedMsg(const char* msg);
bool mapperYesNoDialog(const char* msg);
void mapperShowMessage(const char* msg);
} // namespace fallout
#endif /* FALLOUT_MAPPER_MP_UTILS_H_ */
+2
View File
@@ -11,6 +11,8 @@
namespace fallout {
bool SystemSettings::executableIsMapper() const { return compat_stricmp(executable.c_str(), "mapper") == 0; }
struct SettingDescriptor {
std::function<void()> read;
std::function<void(bool onlyAdd)> write;
+3 -2
View File
@@ -24,6 +24,8 @@ struct SystemSettings {
int free_space = 20480;
int times_run = 0;
std::string screenshots_format = "png";
bool executableIsMapper() const;
};
struct ScreenSettings {
@@ -165,8 +167,7 @@ struct MapperSettings {
// CE: switch between vanilla grid item picker and simpler list-based one.
bool use_grid_item_picker = true;
// CE: change mapper path for saving various data.
// TODO: use
std::string dev_path = R"(\fallout\cd\)";
std::string dev_path;
};
struct Settings {
+20
View File
@@ -56,6 +56,26 @@ int fontManagerAdd(FontManager* fontManager);
int fontGetCurrent();
void fontSetCurrent(int font);
class ScopedFont {
public:
ScopedFont(int font)
: _previousFont(fontGetCurrent())
{
fontSetCurrent(font);
}
~ScopedFont()
{
fontSetCurrent(_previousFont);
}
ScopedFont(const ScopedFont&) = delete;
ScopedFont& operator=(const ScopedFont&) = delete;
private:
int _previousFont;
};
} // namespace fallout
#endif /* TEXT_FONT_H */
+42 -27
View File
@@ -287,6 +287,21 @@ static int gTileWindowWidth;
// 0x66BE34 tile_center_tile
int gCenterTile;
// Optional mapper overlay drawn over the iso view each refresh (edge editor). Null when unused.
static TileMapperOverlayProc* gTileMapperOverlayProc = nullptr;
void tileSetMapperOverlayProc(TileMapperOverlayProc* proc)
{
gTileMapperOverlayProc = proc;
}
void tileMapperOverlayRender(unsigned char* buffer, int pitch, int elevation, const Rect* clip)
{
if (gTileMapperOverlayProc != nullptr) {
gTileMapperOverlayProc(buffer, pitch, elevation, clip);
}
}
// 0x4B0C40 tile_init
int tileInit(TileData** squareGrid, int squareGridWidth, int squareGridHeight, int hexGridWidth, int hexGridHeight, unsigned char* buf, int windowWidth, int windowHeight, int windowPitch, TileWindowRefreshProc* windowRefreshProc)
{
@@ -454,7 +469,7 @@ int tileInit(TileData** squareGrid, int squareGridWidth, int squareGridHeight, i
tileSetCenter(hexGridWidth * (hexGridHeight / 2) + hexGridWidth / 2, TILE_SET_CENTER_FLAG_IGNORE_SCROLL_RESTRICTIONS);
if (compat_stricmp(settings.system.executable.c_str(), "mapper") == 0) {
if (settings.system.executableIsMapper()) {
gTileWindowRefreshElevationProc = tileRefreshMapper;
}
@@ -543,30 +558,16 @@ int tileSetCenter(int tile, int flags)
return -1;
}
bool boundaryModsSet = false;
if (mapEdgeIsLoaded() && !settings.ui.ignore_map_edges) {
bool isScroll = flags == 0;
if (!isScroll) {
// Forced positioning (teleport, initial load, etc.): clamp to edge boundary.
tile = mapEdgeSelectZoneAndClamp(tile, gElevation);
if (!tileIsValid(tile)) return -1;
} else if (mapEdgeZoneIsSelected()) {
// Normal scroll: block if tile is outside boundary (matching sfall CheckBorder).
// Clamping here would move the center slightly and cause mapScroll's buffer
// copy to produce visual artifacts; blocking preserves the current view.
if (!mapEdgeTileInBounds(tile)) {
return -1;
}
// Tile is in bounds; set sub-tile boundary mods if tile is on the edge.
// If the tile is exactly on a boundary edge, force a full redraw
// (matching sfall's CheckBorder returning 1 → modeFlags |= 1).
if (mapEdgeSetBoundaryMods(tile)) {
boundaryModsSet = true;
flags |= TILE_SET_CENTER_REFRESH_WINDOW;
}
}
const bool edgeActive = mapEdgeIsEnabled();
const bool isScroll = flags == 0;
if (edgeActive && !isScroll) {
// Forced positioning (teleport, load): clamp to edge boundary.
tile = mapEdgeSelectZoneAndClamp(tile, gElevation);
if (!tileIsValid(tile)) return -1;
}
bool boundaryModsSet = false;
if ((flags & TILE_SET_CENTER_FLAG_IGNORE_SCROLL_RESTRICTIONS) == 0) {
if (gTileScrollLimitingEnabled) {
int tileScreenX;
@@ -588,9 +589,21 @@ int tileSetCenter(int tile, int flags)
}
}
// Scroll-blocker object check only runs when no EDG is loaded.
// EDG clamping above already enforces the boundary.
if ((!mapEdgeIsLoaded() || !mapEdgeZoneIsSelected()) && gTileScrollBlockingEnabled) {
// Must run after scroll limiting: mapEdgeSetBoundaryMods mutates the persistent
// alignment mods, so a scroll the limiter rejects must not touch them.
if (edgeActive && isScroll && mapEdgeZoneIsSelected()) {
// Block instead of clamp: clamping would shift the center and make
// mapScroll's buffer copy produce artifacts.
if (!mapEdgeTileInBounds(tile)) {
return -1;
}
// On a boundary edge: set sub-tile mods and force a full redraw.
if (mapEdgeSetBoundaryMods(tile)) {
boundaryModsSet = true;
flags |= TILE_SET_CENTER_REFRESH_WINDOW;
}
} else if ((!edgeActive || !mapEdgeZoneIsSelected()) && gTileScrollBlockingEnabled) {
// Object scroll-blocker only applies when EDG isn't enforcing the boundary.
if (_obj_scroll_blocking_at(tile, gElevation) == 0) {
return -1;
}
@@ -712,6 +725,8 @@ static void tileRefreshMapper(Rect* rect, int elevation)
tile_hires_stencil_draw(&rectToUpdate, gTileWindowBuffer, gTileWindowWidth, gTileWindowHeight);
}
tileMapperOverlayRender(gTileWindowBuffer, gTileWindowPitch, elevation, &rectToUpdate);
gTileWindowRefreshProc(&rectToUpdate);
}
@@ -1530,7 +1545,7 @@ void tileRenderFloorsInRect(Rect* rect, int elevation)
// Port of sfall HRP ViewMap::square_obj_render
void tileRenderEdgeBlackSquares(Rect* rect, int elevation, bool drawOnTop)
{
if (!mapEdgeHasSquareRect(elevation)) {
if (!mapEdgeIsEnabled() || !mapEdgeHasSquareRect(elevation)) {
return;
}
+6
View File
@@ -12,6 +12,12 @@ namespace fallout {
typedef void(TileWindowRefreshProc)(Rect* rect);
typedef void(TileWindowRefreshElevationProc)(Rect* rect, int elevation);
// Optional overlay drawn over the mapper iso view each refresh (e.g. the edge editor).
// clip is the region being refreshed. Set to nullptr to disable.
typedef void(TileMapperOverlayProc)(unsigned char* buffer, int pitch, int elevation, const Rect* clip);
void tileSetMapperOverlayProc(TileMapperOverlayProc* proc);
void tileMapperOverlayRender(unsigned char* buffer, int pitch, int elevation, const Rect* clip);
extern const int _off_tile[6];
extern const int dword_51D984[6];
extern int gHexGridSize;
+7 -3
View File
@@ -1,6 +1,7 @@
#include "tile_hires_stencil.h"
#include "debug.h"
#include "draw.h"
#include "game.h"
#include "geometry.h"
#include "map_edge.h"
#include "settings.h"
@@ -65,7 +66,7 @@ static_assert(screen_view_width % (2 * square_width) == 0);
// which is covered by squares but theoretically could be seen in the original game
static_assert(screen_view_height % (2 * square_height) == 20);
static bool gIsTileHiresStencilEnabled = true;
static bool gIsTileHiresStencilEnabled = false;
static void clean_cache()
{
@@ -213,7 +214,7 @@ void tile_hires_stencil_on_center_tile_or_elevation_change()
}
// With EDG loaded, the EdgeClipping path handles blackening via ClearRect/CheckRect.
// The stencil used as a backup when no EDG is present.
if (mapEdgeIsLoaded() || !gTileBorderInitialized || visited_tiles[gElevation][gCenterTile]) {
if (mapEdgeIsEnabled() || !gTileBorderInitialized || visited_tiles[gElevation][gCenterTile]) {
return;
}
@@ -396,7 +397,7 @@ void tile_hires_stencil_draw(Rect* rect, unsigned char* buffer, int windowWidth,
void tile_hires_stencil_init()
{
gIsTileHiresStencilEnabled = settings.ui.enable_high_resolution_stencil;
gIsTileHiresStencilEnabled = !settings.system.executableIsMapper() && settings.ui.enable_high_resolution_stencil;
if (!gIsTileHiresStencilEnabled) {
return;
}
@@ -416,6 +417,9 @@ void tile_hires_stencil_init()
void tile_hires_stencil_on_map_load()
{
if (!gIsTileHiresStencilEnabled) {
return;
}
clean_cache();
tile_hires_stencil_on_center_tile_or_elevation_change();
tileWindowRefresh();

Some files were not shown because too many files have changed in this diff Show More