Compare commits

..
Author SHA1 Message Date
Mike Klaas d2de6715dd more 2026-05-25 14:58:55 -07:00
Mike Klaas df4b787e86 prototype 1 2026-05-24 17:35:26 -07:00
Mike Klaas 6b442546cc better validation of transparency 2026-05-13 14:20:37 -07:00
Mike Klaas 2c08cc0b3a Switch 2-col inventory .frms to .png 2026-05-12 23:53:17 -07:00
Mike Klaas 09a1971cd8 Ability to load indexed pngs in game 2026-05-12 23:02:21 -07:00
32 changed files with 1576 additions and 837 deletions
+2 -2
View File
@@ -161,8 +161,6 @@ set(FALLOUT_ENGINE_SOURCES
"src/map_defs.h"
"src/map.cc"
"src/map.h"
"src/map_edge.cc"
"src/map_edge.h"
"src/memory_defs.h"
"src/memory_manager.cc"
"src/memory_manager.h"
@@ -207,6 +205,8 @@ set(FALLOUT_ENGINE_SOURCES
"src/proto_types.h"
"src/proto.cc"
"src/proto.h"
"src/prototype_options.cc"
"src/prototype_options.h"
"src/queue.cc"
"src/queue.h"
"src/random.cc"
-12
View File
@@ -4,18 +4,6 @@ This document tracks Fallout 2 CE compatibility with sfall. This is for modders
For now, this covers opcodes/metarules, and hooks. In the future, it will include other ways of modifying the engine (like ini files), and other Sfall-specific behaviour.
## HRP EDG Scroll-Blocker Support
CE supports the `.edg` file format from the HRP (High Resolution Patch), which defines per-map scroll boundaries and square-level render clipping.
**How it works in CE:**
- On map load, `maps/<mapname>.edg` is read if present. Missing file = silent fallback to the scroll-blocker object system.
- The `.edg` file defines per-elevation rectangle boundary zones. Multiple chained zones per elevation are supported.
- When loaded, these zones are used for both scroll blocking and visible area clipping (black bars), replacing both vanilla scroll blocking and CE hi-res stencil system.
- v2 EDG files also contain a `SquareRect` that defines "Angled edges", or square-grid stencil. This is also supported.
## Settings (ddraw.ini → fallout2.cfg / game.cfg)
Settings previously read from `ddraw.ini` have been moved into standard CE config files.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

-2
View File
@@ -110,8 +110,6 @@ iface_bar_mode=0
iface_bar_side_art=2
iface_bar_sides_ori=0
iface_bar_width=800
;Whether to load EDG files (HRP format) when loading maps. If loaded, they override default edge clipping and scroll blocking behavior.
edg_support=1
ignore_map_edges=0
;Set to 1 to display numbered dialogue options
numbers_in_dialogue=0
+161 -9
View File
@@ -1,6 +1,9 @@
#include "art.h"
#include <lodepng.h>
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -134,6 +137,7 @@ static int* gArtCritterFidShoudRunData;
static std::unordered_map<std::string, std::shared_ptr<NamedCacheEntry>> gNamedArtCache;
constexpr int kNamedCacheMaxBytes = 32 * 1024 * 1024; // 32MB soft limit
constexpr size_t kMaxNamedPngPixels = 16 * 1024 * 1024;
static unsigned int gNamedArtCacheMruCounter = 0;
static int gNamedArtCacheCurrentBytes = 0;
@@ -1116,7 +1120,7 @@ static int artReadFrameData(unsigned char* data, File* stream, int count, int* p
// 0x419E1C
static int artReadHeader(Art* art, File* stream)
{
if (fileReadInt32(stream, &(art->field_0)) == -1) return -1;
if (fileReadInt32(stream, &(art->version)) == -1) return -1;
if (fileReadInt16(stream, &(art->framesPerSecond)) == -1) return -1;
if (fileReadInt16(stream, &(art->actionFrame)) == -1) return -1;
if (fileReadInt16(stream, &(art->frameCount)) == -1) return -1;
@@ -1133,13 +1137,137 @@ static int artReadHeader(Art* art, File* stream)
return 0;
}
// NOTE: Original function was slightly different, but never used. Basically
// it's a memory allocating variant of `artRead` (which reads data into given
// buffer). This function is useful to load custom `frm` files since `Art` now
// needs more memory then it's on-disk size (due to memory padding).
//
// 0x419EC0
Art* artLoad(const char* path)
static bool artPathHasExtension(const char* path, const char* extension)
{
size_t pathLength = strlen(path);
size_t extensionLength = strlen(extension);
if (pathLength < extensionLength) {
return false;
}
return compat_stricmp(path + pathLength - extensionLength, extension) == 0;
}
static bool artReadFile(const char* path, std::vector<unsigned char>& data)
{
int size = 0;
if (dbGetFileSize(path, &size) != 0 || size <= 0) {
return false;
}
data.resize(size);
return dbGetFileContents(path, data.data()) == 0;
}
static bool artUnpackIndexedPngPixels(const std::vector<unsigned char>& indexedData, unsigned width, unsigned height, unsigned bitdepth, unsigned char* output)
{
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (bitdepth == 8) {
if (indexedData.size() < pixelCount) {
return false;
}
memcpy(output, indexedData.data(), pixelCount);
return true;
}
unsigned char mask = static_cast<unsigned char>((1 << bitdepth) - 1);
size_t neededBytes = (pixelCount * bitdepth + 7) / 8;
if (indexedData.size() < neededBytes) {
return false;
}
for (size_t index = 0; index < pixelCount; index++) {
size_t bitOffset = index * bitdepth;
unsigned char byte = indexedData[bitOffset / 8];
unsigned shift = 8 - bitdepth - static_cast<unsigned>(bitOffset % 8);
output[index] = (byte >> shift) & mask;
}
return true;
}
static Art* artLoadIndexedPng(const char* path)
{
std::vector<unsigned char> encoded;
if (!artReadFile(path, encoded)) {
return nullptr;
}
lodepng::State state;
state.decoder.color_convert = 0;
std::vector<unsigned char> indexedData;
unsigned width = 0;
unsigned height = 0;
unsigned error = lodepng::decode(indexedData, width, height, state, encoded);
if (error != 0) {
debugPrint("ART: failed to decode indexed PNG %s: %s\n", path, lodepng_error_text(error));
return nullptr;
}
if (state.info_png.color.colortype != LCT_PALETTE) {
debugPrint("ART: PNG is not palette-indexed: %s\n", path);
return nullptr;
}
if (lodepng_has_palette_alpha(&state.info_png.color)) {
debugPrint("ART: indexed PNG transparency is unsupported, reserve palette index 0 instead: %s\n", path);
return nullptr;
}
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (width == 0 || height == 0 || width > SHRT_MAX || height > SHRT_MAX) {
debugPrint("ART: invalid indexed PNG dimensions for %s: %ux%u\n", path, width, height);
return nullptr;
}
if (pixelCount > kMaxNamedPngPixels || pixelCount > INT_MAX) {
debugPrint("ART: indexed PNG is too large: %s\n", path);
return nullptr;
}
Art header = {};
header.version = 4;
header.framesPerSecond = 10;
header.actionFrame = 0;
header.frameCount = 1;
header.dataSize = sizeof(ArtFrame) + static_cast<int>(pixelCount);
int currentPadding = paddingForSize(sizeof(Art));
for (int rotation = 0; rotation < ROTATION_COUNT; rotation++) {
header.dataOffsets[rotation] = 0;
header.padding[rotation] = currentPadding;
}
int dataSize = artGetDataSize(&header);
unsigned char* data = reinterpret_cast<unsigned char*>(internal_malloc(dataSize));
if (data == nullptr) {
return nullptr;
}
memset(data, 0, dataSize);
Art* art = reinterpret_cast<Art*>(data);
*art = header;
ArtFrame* frame = reinterpret_cast<ArtFrame*>(data + sizeof(Art) + art->padding[0]);
frame->width = static_cast<short>(width);
frame->height = static_cast<short>(height);
frame->size = static_cast<int>(pixelCount);
frame->x = 0;
frame->y = 0;
if (!artUnpackIndexedPngPixels(indexedData, width, height, state.info_png.color.bitdepth, reinterpret_cast<unsigned char*>(frame) + sizeof(ArtFrame))) {
debugPrint("ART: failed to read indexed PNG pixels: %s\n", path);
internal_free(data);
return nullptr;
}
return art;
}
static Art* artLoadFrm(const char* path)
{
File* stream = fileOpen(path, "rb");
if (stream == nullptr) {
@@ -1167,6 +1295,30 @@ Art* artLoad(const char* path)
return reinterpret_cast<Art*>(data);
}
// NOTE: Original function was slightly different, but never used. Basically
// it's a memory allocating variant of `artRead` (which reads data into given
// buffer). This function is useful to load custom `frm` files since `Art` now
// needs more memory then it's on-disk size (due to memory padding).
//
// 0x419EC0
Art* artLoad(const char* path)
{
if (path == nullptr) {
return nullptr;
}
if (artPathHasExtension(path, ".png")) {
return artLoadIndexedPng(path);
}
Art* art = artLoadFrm(path);
if (art != nullptr) {
return art;
}
return nullptr;
}
static Art* artLoadLocalized(const char* path)
{
const char* localizedPath;
@@ -1239,7 +1391,7 @@ int artWriteFrameData(unsigned char* data, File* stream, int count)
// 0x41A138
int artWriteHeader(Art* art, File* stream)
{
if (fileWriteInt32(stream, art->field_0) == -1) return -1;
if (fileWriteInt32(stream, art->version) == -1) return -1;
if (fileWriteInt16(stream, art->framesPerSecond) == -1) return -1;
if (fileWriteInt16(stream, art->actionFrame) == -1) return -1;
if (fileWriteInt16(stream, art->frameCount) == -1) return -1;
+1 -1
View File
@@ -69,7 +69,7 @@ typedef enum Background {
} Background;
typedef struct Art {
int field_0;
int version;
short framesPerSecond;
short actionFrame;
short frameCount;
-9
View File
@@ -24,7 +24,6 @@
#include "interface.h"
#include "item.h"
#include "kb.h"
#include "map_edge.h"
#include "mouse.h"
#include "object.h"
#include "proto.h"
@@ -932,9 +931,6 @@ void _gmouse_handle_event(int mouseX, int mouseY, int mouseState)
if (gameMouseClickOnInterfaceBar()) {
return;
}
if (mapEdgeIsOverClippedArea(mouseX, mouseY)) {
return;
}
// Check if we should block mouse button up events for inventoryOpenUseItemOn inventory window
if (gBlockMouseUpEvent && (mouseState & MOUSE_EVENT_LEFT_BUTTON_UP) != 0) {
@@ -2451,11 +2447,6 @@ int gameMouseHandleScrolling(int x, int y, int cursor)
}
if (dx == 0 && dy == 0) {
if (mapEdgeIsOverClippedArea(x, y)) {
gameMouseObjectsHide();
gameMouseSetCursor(MOUSE_CURSOR_ARROW);
return 0;
}
return -1;
}
+4 -4
View File
@@ -779,9 +779,9 @@ static void inventoryLootApplyLayout(int columns)
static void inventoryNormalLayoutUpdate()
{
int columns = inventoryChooseColumns(inventoryFrmImage, INVENTORY_WINDOW_WIDTH + INVENTORY_SLOT_WIDTH, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "invbox2.frm");
int columns = inventoryChooseColumns(inventoryFrmImage, INVENTORY_WINDOW_WIDTH + INVENTORY_SLOT_WIDTH, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "INVBOX2.PNG");
if (columns == 1) {
inventoryBackgroundLoad(inventoryFrmImage, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "invbox2.frm", 1);
inventoryBackgroundLoad(inventoryFrmImage, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "INVBOX2.PNG", 1);
}
inventoryNormalApplyLayout(columns);
@@ -789,9 +789,9 @@ static void inventoryNormalLayoutUpdate()
static void inventoryLootLayoutUpdate()
{
int columns = inventoryChooseColumns(inventoryLootFrmImage, INVENTORY_LOOT_WINDOW_WIDTH_EXPANDED, INVENTORY_LOOT_BACKGROUND_FRM_ID, "loot2.frm");
int columns = inventoryChooseColumns(inventoryLootFrmImage, INVENTORY_LOOT_WINDOW_WIDTH_EXPANDED, INVENTORY_LOOT_BACKGROUND_FRM_ID, "LOOT2.png");
if (columns == 1) {
inventoryBackgroundLoad(inventoryLootFrmImage, INVENTORY_LOOT_BACKGROUND_FRM_ID, "loot2.frm", 1);
inventoryBackgroundLoad(inventoryLootFrmImage, INVENTORY_LOOT_BACKGROUND_FRM_ID, "LOOT2.png", 1);
}
inventoryLootApplyLayout(columns);
+5
View File
@@ -147,6 +147,11 @@ int _kb_getch()
return rc;
}
int keyboardGetLastScanCode()
{
return gLastKeyboardEvent.scanCode;
}
// 0x4CBE00 kb_disable
void keyboardDisable()
{
+1
View File
@@ -346,6 +346,7 @@ int keyboardInit();
void keyboardFree();
void keyboardReset();
int _kb_getch();
int keyboardGetLastScanCode();
void keyboardDisable();
void keyboardEnable();
int keyboardIsDisabled();
+14 -2
View File
@@ -30,6 +30,7 @@
#include "platform_compat.h"
#include "preferences.h"
#include "proto.h"
#include "prototype_options.h"
#include "random.h"
#include "scripts.h"
#include "settings.h"
@@ -75,6 +76,7 @@ static bool _main_show_death_scene = false;
static bool _main_death_voiceover_done;
static int commandLineDevLoadGameSlot = -1;
static bool commandLineDevOptionsMenu = false;
// 0x48099C
int falloutMain(int argc, char** argv)
@@ -111,7 +113,10 @@ int falloutMain(int argc, char** argv)
mouseShowCursor();
int devLoadGameSlot = commandLineDevLoadGameSlot;
int mainMenuRc;
if (devLoadGameSlot != -1) {
if (commandLineDevOptionsMenu) {
commandLineDevOptionsMenu = false;
mainMenuRc = MAIN_MENU_PROTOTYPE_OPTIONS;
} else if (devLoadGameSlot != -1) {
commandLineDevLoadGameSlot = -1;
mainMenuRc = MAIN_MENU_LOAD_GAME;
} else {
@@ -217,6 +222,10 @@ int falloutMain(int argc, char** argv)
mainMenuWindowHide(true);
doPreferences(true);
break;
case MAIN_MENU_PROTOTYPE_OPTIONS:
mainMenuWindowHide(true);
showPrototypeOptionsMenu(true);
break;
case MAIN_MENU_CREDITS:
mainMenuWindowHide(true);
creditsOpen("credits.txt", -1, false);
@@ -264,9 +273,12 @@ static void mainParseCommandLineArguments(int argc, char** argv)
{
const char* devLoadGamePrefix = "--dev-load-game=";
size_t devLoadGamePrefixLength = strlen(devLoadGamePrefix);
const char* devOptionsMenuFlag = "--dev-options-menu";
for (int arg = 1; arg < argc; arg += 1) {
if (strncmp(argv[arg], devLoadGamePrefix, devLoadGamePrefixLength) == 0) {
if (strcmp(argv[arg], devOptionsMenuFlag) == 0) {
commandLineDevOptionsMenu = true;
} else if (strncmp(argv[arg], devLoadGamePrefix, devLoadGamePrefixLength) == 0) {
int slot;
if (mainTryParseDevLoadGameSlot(argv[arg] + devLoadGamePrefixLength, &slot)) {
commandLineDevLoadGameSlot = slot;
+4
View File
@@ -355,6 +355,10 @@ int mainMenuWindowHandleEvents()
if (keyCode == KEY_CTRL_R) {
rc = MAIN_MENU_SELFRUN;
continue;
} else if (keyCode == KEY_UPPERCASE_Z || keyCode == KEY_LOWERCASE_Z) {
main_menu_play_sound("nmselec1");
rc = MAIN_MENU_PROTOTYPE_OPTIONS;
continue;
} else if (keyCode == KEY_PLUS || keyCode == KEY_EQUAL) {
brightnessIncrease();
} else if (keyCode == KEY_MINUS || keyCode == KEY_UNDERSCORE) {
+1
View File
@@ -14,6 +14,7 @@ typedef enum MainMenuOption {
MAIN_MENU_EXIT,
MAIN_MENU_SELFRUN,
MAIN_MENU_OPTIONS,
MAIN_MENU_PROTOTYPE_OPTIONS,
} MainMenuOption;
int mainMenuWindowInit();
+8 -34
View File
@@ -26,7 +26,6 @@
#include "item.h"
#include "light.h"
#include "loadsave.h"
#include "map_edge.h"
#include "memory.h"
#include "object.h"
#include "palette.h"
@@ -226,9 +225,7 @@ int isoInit()
colorCycleInit();
debugPrint(">cycle_init\t\t");
if (!settings.ui.ignore_map_edges) {
tileScrollBlockingEnable();
}
tileScrollBlockingEnable();
tileScrollLimitingEnable();
if (interfaceInit() != 0) {
@@ -737,7 +734,6 @@ int mapSetEnteringLocation(int elevation, int tile_num, int orientation)
// 0x482938
void mapNewMap()
{
mapEdgeFree();
mapSetElevation(0);
tileSetCenter(20100, TILE_SET_CENTER_FLAG_IGNORE_SCROLL_RESTRICTIONS);
memset(&gMapTransition, 0, sizeof(gMapTransition));
@@ -935,10 +931,6 @@ static int mapLoad(File* stream)
goto err;
}
if (settings.ui.edg_support) {
mapEdgeLoad(gMapHeader.name);
}
error = "Error setting tile center";
if (tileSetCenter(gEnteringTile, TILE_SET_CENTER_FLAG_IGNORE_SCROLL_RESTRICTIONS) != 0) {
goto err;
@@ -1069,7 +1061,7 @@ err:
// NOTE: Uninline.
mapSetEnteringLocation(-1, -1, -1);
tile_hires_stencil_on_map_load();
tile_hires_stencil_init();
gameMovieFadeOut();
@@ -1540,7 +1532,7 @@ static void isoWindowRefreshRect(Rect* rect)
windowRefreshRect(gIsoWindow, rect);
}
// 0x483EE4 map_scroll_refresh_game
// 0x483EE4
static void isoWindowRefreshRectGame(Rect* rect)
{
Rect rectToUpdate;
@@ -1548,31 +1540,23 @@ static void isoWindowRefreshRectGame(Rect* rect)
return;
}
Rect visArea;
bool hasVisArea = mapEdgeComputeVisibleArea(gElevation, &visArea);
// HRP rect_inside_bound_scroll_clip: always clear srcRect, then clip to mapVisibleArea.
// CE: Clear dirty rect to prevent most of the visual artifacts near map
// edges.
bufferFill(gIsoWindowBuffer + rectToUpdate.top * rectGetWidth(&gIsoWindowRect) + rectToUpdate.left,
rectGetWidth(&rectToUpdate),
rectGetHeight(&rectToUpdate),
rectGetWidth(&gIsoWindowRect),
0);
if (hasVisArea && rectIntersection(&rectToUpdate, &visArea, &rectToUpdate) == -1) {
return;
}
tileRenderFloorsInRect(&rectToUpdate, gElevation);
_obj_render_pre_roof(&rectToUpdate, gElevation);
tileRenderRoofsInRect(&rectToUpdate, gElevation);
_obj_render_post_roof(&rectToUpdate, gElevation);
if (!hasVisArea) {
tile_hires_stencil_draw(&rectToUpdate, gIsoWindowBuffer, rectGetWidth(&gIsoWindowRect), rectGetHeight(&gIsoWindowRect));
}
tile_hires_stencil_draw(&rectToUpdate, gIsoWindowBuffer, rectGetWidth(&gIsoWindowRect), rectGetHeight(&gIsoWindowRect));
}
// 0x483F44 map_scroll_refresh_mapper
// 0x483F44
static void isoWindowRefreshRectMapper(Rect* rect)
{
Rect rectToUpdate;
@@ -1580,29 +1564,19 @@ static void isoWindowRefreshRectMapper(Rect* rect)
return;
}
Rect visArea;
bool hasVisArea = mapEdgeComputeVisibleArea(gElevation, &visArea);
// HRP rect_inside_bound_scroll_clip: always clear srcRect, then clip to mapVisibleArea.
bufferFill(gIsoWindowBuffer + rectToUpdate.top * rectGetWidth(&gIsoWindowRect) + rectToUpdate.left,
rectGetWidth(&rectToUpdate),
rectGetHeight(&rectToUpdate),
rectGetWidth(&gIsoWindowRect),
0);
if (hasVisArea && rectIntersection(&rectToUpdate, &visArea, &rectToUpdate) == -1) {
return;
}
tileRenderFloorsInRect(&rectToUpdate, gElevation);
_grid_render(&rectToUpdate, gElevation);
_obj_render_pre_roof(&rectToUpdate, gElevation);
tileRenderRoofsInRect(&rectToUpdate, gElevation);
_obj_render_post_roof(&rectToUpdate, gElevation);
if (!hasVisArea) {
tile_hires_stencil_draw(&rectToUpdate, gIsoWindowBuffer, rectGetWidth(&gIsoWindowRect), rectGetHeight(&gIsoWindowRect));
}
tile_hires_stencil_draw(&rectToUpdate, gIsoWindowBuffer, rectGetWidth(&gIsoWindowRect), rectGetHeight(&gIsoWindowRect));
}
// NOTE: Inlined.
-462
View File
@@ -1,462 +0,0 @@
#include "map_edge.h"
#include "db.h"
#include "debug.h"
#include "geometry.h"
#include "map.h"
#include "map_defs.h"
#include "platform_compat.h"
#include "svga.h"
#include "tile.h"
#include "window_manager.h"
#include <cassert>
#include <memory>
namespace fallout {
// Tile size in pixels
constexpr int kTileWidth = 32;
constexpr int kTileHeight = 24;
static std::unique_ptr<EdgeZone> gEdgeZones[ELEVATION_COUNT];
static bool gEdgeDataLoaded = false;
static bool gEdgeVersion2 = false;
static EdgeZone* gCurrentEdgeZone = nullptr;
// Boundary alignment mods (sfall mapModWidth/Height), set by clamp/check functions.
static int currentTileXAlignment = 0;
static int currentTileYAlignment = 0;
static int maxTileXAlignment = 0;
static int maxTileYAlignment = 0;
static Rect gMapVisibleArea;
// Convert tile index to pixel-offset coordinates.
// Equivalent to sfall ViewMap::GetTileCoordOffset.
void tileToPixelOffset(int tile, int& outX, int& outY)
{
int x = tile % HEX_GRID_WIDTH;
int y = (tile / HEX_GRID_WIDTH) + (x / 2);
y &= ~1; // force even row
x = (2 * x) + 200 - y;
outY = kTileHeight / 2 * y;
outX = kTileWidth / 2 * x;
}
// Convert pixel-offset to tile coord (in-place, like sfall GetCoordFromOffset).
void pixelToTileCoord(int& inOutX, int& inOutY)
{
int y = inOutY / kTileHeight;
int x = (inOutX / kTileWidth) + y - 100;
inOutX = x;
inOutY = (2 * y) - (x / 2);
}
// Convert pixel-offset to tile number.
static int pixelToTile(int px, int py)
{
pixelToTileCoord(px, py);
return px + py * HEX_GRID_WIDTH;
}
static Size getIsoWindowSize()
{
Rect winRect;
assert(windowGetRect(gIsoWindow, &winRect) != -1);
return { rectGetWidth(&winRect), rectGetHeight(&winRect) };
}
// Fill pixel-space fields of a zone from its stored tileRect corners.
// Screen-size dependent — must be re-run if resolution changes.
static void calcEdgeData(EdgeZone* zone)
{
const auto [winWidth, winHeight] = getIsoWindowSize();
const int winHalfWidth = winWidth / 2;
const int winHalfHeight = winHeight / 2;
// Compute sub-tile alignment sizes (sfall mapWidthModSize / mapHeightModSize).
maxTileXAlignment = winHalfWidth % kTileWidth;
maxTileYAlignment = winHalfHeight % kTileHeight;
// Truncated half sizes for border contraction (matching sfall ViewMap::GetWinMapHalfSize).
const int winHalfWidthSnapped = winHalfWidth - maxTileXAlignment;
const int winHalfHeightSnapped = winHalfHeight - maxTileYAlignment;
// Convert tileRect corners to pixel offsets.
// pixelRect = raw pixel-space rect before any contraction.
int px, py;
tileToPixelOffset(zone->tileRect.left, px, py);
zone->pixelRect.left = zone->scrollBorderRect.left = px;
tileToPixelOffset(zone->tileRect.top, px, py);
zone->pixelRect.top = zone->scrollBorderRect.top = py;
tileToPixelOffset(zone->tileRect.right, px, py);
zone->pixelRect.right = zone->scrollBorderRect.right = px;
tileToPixelOffset(zone->tileRect.bottom, px, py);
zone->pixelRect.bottom = zone->scrollBorderRect.bottom = py;
// Contract scrollBorderRect inward by window half-size (or half its own size snapped to hex grid, if it's smaller than the window).
// Narrows the scrollable area, with guards below collapsing to a point if it crosses.
{
long rectHalfWidth = (zone->scrollBorderRect.left - zone->scrollBorderRect.right) / 2;
if (rectHalfWidth < winHalfWidthSnapped) {
long remainder = rectHalfWidth % kTileWidth;
long rectHalfWidthSnapped = rectHalfWidth - remainder;
zone->scrollBorderRect.left -= rectHalfWidthSnapped;
zone->scrollBorderRect.right += rectHalfWidthSnapped + (remainder ? kTileWidth : 0);
} else {
zone->scrollBorderRect.left -= winHalfWidthSnapped;
zone->scrollBorderRect.right += winHalfWidthSnapped;
}
}
{
long rectHalfHeight = (zone->scrollBorderRect.bottom - zone->scrollBorderRect.top) / 2;
if (rectHalfHeight < winHalfHeightSnapped) {
long remainder = rectHalfHeight % kTileHeight;
long rectHalfHeightSnapped = rectHalfHeight - remainder;
zone->scrollBorderRect.top += rectHalfHeightSnapped + (remainder ? kTileHeight : 0);
zone->scrollBorderRect.bottom -= rectHalfHeightSnapped;
} else {
zone->scrollBorderRect.top += winHalfHeightSnapped;
zone->scrollBorderRect.bottom -= winHalfHeightSnapped;
}
}
// Inverted-X guard: collapse to vertical line if left/right cross or nearly touch.
if ((zone->scrollBorderRect.left < zone->scrollBorderRect.right) || (zone->scrollBorderRect.left - zone->scrollBorderRect.right) == kTileWidth) {
zone->scrollBorderRect.left = zone->scrollBorderRect.right;
}
// Inverted-Y guard
if ((zone->scrollBorderRect.bottom < zone->scrollBorderRect.top) || (zone->scrollBorderRect.bottom - zone->scrollBorderRect.top) == kTileHeight) {
zone->scrollBorderRect.bottom = zone->scrollBorderRect.top;
}
debugPrint("EDG[%p] tileRect=(%d,%d,%d,%d) win=(%d,%d) half size snap=(%d,%d) pixelRect=(%d,%d,%d,%d) scrollBorderRect=(%d,%d,%d,%d)\n",
static_cast<void*>(zone), zone->tileRect.left, zone->tileRect.top,
zone->tileRect.right, zone->tileRect.bottom, winWidth, winHeight, winHalfWidthSnapped, winHalfHeightSnapped,
zone->pixelRect.left, zone->pixelRect.top,
zone->pixelRect.right, zone->pixelRect.bottom,
zone->scrollBorderRect.left, zone->scrollBorderRect.top,
zone->scrollBorderRect.right, zone->scrollBorderRect.bottom);
}
// Multi-edge zone selection: pick the zone whose scrollBorderRect contains the pixel position.
// Matches GetCenterTile logic: advance while target is outside current zone, use last if
// none contains it.
static EdgeZone* findZoneByPixel(int px, int py, int elevation)
{
EdgeZone* zone = gEdgeZones[elevation].get();
if (zone == nullptr) {
return nullptr;
}
// Multi-edge: advance while target is outside current zone.
// 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) {
// Point is inside current zone.
if (px < zone->pixelRect.left && px > zone->pixelRect.right
&& py > zone->pixelRect.top - kZoneMarginY && py < zone->pixelRect.bottom - kZoneMarginY) {
break;
}
zone = zone->next.get();
}
return zone;
}
// Load EDG file, populate gEdgeZones, and compute pixel-space fields.
// EDG files use big-endian byte order (like all Fallout 2 files).
static bool mapEdgeLoadFromStream(File* stream)
{
int magic;
if (fileReadInt32(stream, &magic) == -1 || magic != 'EDGE') return false;
int version;
if (fileReadInt32(stream, &version) == -1 || (version != 1 && version != 2)) return false;
int reserved;
if (fileReadInt32(stream, &reserved) == -1 || reserved != 0) return false;
gEdgeVersion2 = (version == 2);
int levelIndicator = 0;
for (int elev = 0; elev < ELEVATION_COUNT; elev++) {
int sqLeft = 99, sqTop = 0, sqRight = 0, sqBottom = 99;
int sqClipData = 0;
if (gEdgeVersion2) {
int sqRect[4];
if (fileReadInt32List(stream, sqRect, 4) == -1
|| fileReadInt32(stream, &sqClipData) == -1) {
return false;
}
sqLeft = sqRect[0];
sqTop = sqRect[1];
sqRight = sqRect[2];
sqBottom = sqRect[3];
}
if (levelIndicator != elev) {
continue; // no tileRect data for this elevation
}
auto tail = &gEdgeZones[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;
if (fileReadInt32(stream, &levelIndicator) == -1) {
return elev == ELEVATION_COUNT - 1;
}
if (levelIndicator != elev) {
break; // next elevation begins
}
}
}
return true;
}
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);
File* stream = fileOpen(edgPath, "rb");
if (stream == nullptr) {
return;
}
bool ok = mapEdgeLoadFromStream(stream);
fileClose(stream);
if (ok) {
gEdgeDataLoaded = true;
debugPrint("mapEdgeLoad: loaded %s\n", edgPath);
} else {
debugPrint("mapEdgeLoad: failed to parse %s\n", edgPath);
mapEdgeFree();
}
}
void mapEdgeFree()
{
for (auto& gEdgeZone : gEdgeZones) {
gEdgeZone.reset();
}
gEdgeDataLoaded = false;
gCurrentEdgeZone = nullptr;
currentTileXAlignment = 0;
currentTileYAlignment = 0;
maxTileXAlignment = 0;
maxTileYAlignment = 0;
gMapVisibleArea = {};
}
bool mapEdgeIsLoaded()
{
return gEdgeDataLoaded;
}
bool mapEdgeZoneIsSelected()
{
return gCurrentEdgeZone != nullptr;
}
// Shared helper: set currentTileXAlignment/Height if the pixel is on a scrollBorderRect edge.
static void updateTileAlignment(const EdgeZone* zone, int px, int py);
int mapEdgeSelectZoneAndClamp(int tile, int elevation)
{
int px, py;
tileToPixelOffset(tile, px, py);
// Set current zone for subsequent scroll blocking checks.
EdgeZone* zone = gCurrentEdgeZone = findZoneByPixel(px, py, elevation);
if (zone == nullptr) {
return tile;
}
// Clamp pixel position to scrollBorderRect (matching sfall GetCenterTile).
// Note: X is inverted (left > right), so the valid range is [right, left].
int cx = (px <= zone->scrollBorderRect.left)
? ((px >= zone->scrollBorderRect.right) ? px : zone->scrollBorderRect.right)
: zone->scrollBorderRect.left;
int cy = (py <= zone->scrollBorderRect.bottom)
? ((py >= zone->scrollBorderRect.top) ? py : zone->scrollBorderRect.top)
: zone->scrollBorderRect.bottom;
updateTileAlignment(zone, cx, cy);
return pixelToTile(cx, cy);
}
bool mapEdgeTileInBounds(int tile)
{
int px, py;
tileToPixelOffset(tile, px, py);
EdgeZone* zone = gCurrentEdgeZone;
if (zone == nullptr) {
return false;
}
// Note: X is inverted (left > right), so check px in [right, left].
return px >= zone->scrollBorderRect.right && px <= zone->scrollBorderRect.left
&& py >= zone->scrollBorderRect.top && py <= zone->scrollBorderRect.bottom;
}
// Shared helper: set currentTileXAlignment if the pixel is on a scrollBorderRect edge.
static void updateTileAlignment(const EdgeZone* zone, int px, int py)
{
currentTileXAlignment = 0;
currentTileYAlignment = 0;
if (px == zone->scrollBorderRect.left) {
currentTileXAlignment = -maxTileXAlignment;
} else if (px == zone->scrollBorderRect.right) {
currentTileXAlignment = maxTileXAlignment;
}
if (py == zone->scrollBorderRect.top) {
currentTileYAlignment = -maxTileYAlignment;
} else if (py == zone->scrollBorderRect.bottom) {
currentTileYAlignment = maxTileYAlignment;
}
}
bool mapEdgeSetBoundaryMods(int tile)
{
int px, py;
tileToPixelOffset(tile, px, py);
EdgeZone* zone = gCurrentEdgeZone;
if (zone == nullptr) {
currentTileXAlignment = 0;
currentTileYAlignment = 0;
return false;
}
const int oldModW = currentTileXAlignment;
const int oldModH = currentTileYAlignment;
updateTileAlignment(zone, px, py);
return currentTileXAlignment != oldModW || currentTileYAlignment != oldModH;
}
int mapEdgeGetTileXAlignment() { return currentTileXAlignment; }
int mapEdgeGetTileYAlignment() { return currentTileYAlignment; }
bool mapEdgeHasSquareRect(int elevation)
{
const auto& zone = gEdgeZones[elevation];
return gEdgeVersion2 && zone != nullptr && zone->squareRect.left >= 0;
}
void mapEdgeGetSquareRect(int elevation, Rect* outRect)
{
const auto& zone = gEdgeZones[elevation];
*outRect = zone->squareRect;
}
EdgeZone::ClipSides mapEdgeGetClipSides(int elevation)
{
const auto& zone = gEdgeZones[elevation];
return zone ? zone->clipSides : EdgeZone::ClipSides{};
}
void mapEdgeRecalc()
{
for (auto& gEdgeZone : gEdgeZones) {
const auto* zone = &gEdgeZone;
while (zone != nullptr) {
calcEdgeData(zone->get());
zone = &zone->get()->next;
}
}
}
bool mapEdgeComputeVisibleArea(int elevation, Rect* outRect)
{
if (!gEdgeDataLoaded) return false;
int px, py;
tileToPixelOffset(gCenterTile, px, py);
px += currentTileXAlignment;
py -= currentTileYAlignment;
EdgeZone* zone = findZoneByPixel(px, py, elevation);
if (zone == nullptr) return false;
// Convert pixel-offset space to screen space via screen origin.
// screenOrigin = pixel-space coord of the screen's top-left corner.
// X is inverted in pixel-space: screenX = screenOriginX - pixelX.
// Matches sfall EdgeClipping::rect_inside_bound_clip mapVisibleArea computation.
const auto [winWidth, winHeight] = getIsoWindowSize();
const int screenOriginX = px + winWidth / 2 - 1;
const int screenOriginY = py - (winHeight / 2 - 1);
outRect->left = screenOriginX - zone->pixelRect.left;
outRect->right = screenOriginX - zone->pixelRect.right;
outRect->top = zone->pixelRect.top - screenOriginY;
outRect->bottom = zone->pixelRect.bottom - screenOriginY;
gMapVisibleArea = *outRect;
return true;
}
bool mapEdgeIsOverClippedArea(int screenX, int screenY)
{
if (!gEdgeDataLoaded) return false;
if (screenX >= gMapVisibleArea.left && screenX <= gMapVisibleArea.right && screenY >= gMapVisibleArea.top && screenY < gMapVisibleArea.bottom) return false;
return windowGetAtPoint(screenX, screenY) == gIsoWindow;
}
} // namespace fallout
-103
View File
@@ -1,103 +0,0 @@
#ifndef MAP_EDGE_H
#define MAP_EDGE_H
#include <memory>
#include "geometry.h"
namespace fallout {
struct EdgeZone {
struct ClipSides {
bool bottom = false;
bool right = false;
bool top = false;
bool left = false;
};
// Raw tile indices from EDG file (tileRect in tile space, 0-39999).
Rect tileRect;
// Pixel-space rect from tileRect corner conversion (before contraction).
Rect pixelRect;
// Pixel-space boundary for center-tile scroll blocking (screen-size dependent).
// Derived from pixelRect by contracting inward by window half-size and snapping to hex grid.
// 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.
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;
};
// 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);
// 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();
// Returns true if a zone was selected on last tileSetCenter call.
bool mapEdgeZoneIsSelected();
// Clamps tile to the EDG boundary for the given elevation.
// Returns the clamped tile (may equal tile if already in bounds).
int mapEdgeSelectZoneAndClamp(int tile, int elevation);
// Returns true if tile is within the EDG boundary for the given elevation.
// Used by the stencil flood-fill as a traversal gate.
bool mapEdgeTileInBounds(int tile);
// Returns true if squareRect data (v2 EDG) is available for this elevation.
bool mapEdgeHasSquareRect(int elevation);
// Convert tile index to pixel-offset coordinates.
// Equivalent to sfall ViewMap::GetTileCoordOffset.
void tileToPixelOffset(int tile, int& outX, int& outY);
// Convert pixel-offset to tile coord (in-place, like sfall GetCoordFromOffset).
void pixelToTileCoord(int& inOutX, int& inOutY);
// Fills the squareRect for the given elevation.
// Only valid when mapEdgeHasSquareRect(elevation) returns true.
void mapEdgeGetSquareRect(int elevation, Rect* outRect);
// Returns the clipSides for the given elevation (default-constructed if no EDG data).
EdgeZone::ClipSides mapEdgeGetClipSides(int elevation);
// Recalculate all pixel-space fields using current screen dimensions.
// Call when resolution changes while a map is loaded.
void mapEdgeRecalc();
// Pixel-offset adjustments for sub-tile boundary alignment (sfall mapModWidth/Height).
// Used when camera is exactly at one of the scroll border edges.
int mapEdgeGetTileXAlignment();
int mapEdgeGetTileYAlignment();
// For the blocking (flags == 0) scroll path: sets boundary mod values if the given
// tile's pixel position is exactly on a scrollBorderRect edge.
// Returns true if the tile is on a boundary edge (mods changed) — caller should set
// TILE_SET_CENTER_REFRESH_WINDOW for full redraw, matching sfall CheckBorder result==1.
bool mapEdgeSetBoundaryMods(int tile);
// Computes the screen-space visible area rectangle.
// Equivalent to sfall EdgeClipping's mapVisibleArea. Returns false if no EDG loaded.
bool mapEdgeComputeVisibleArea(int elevation, Rect* outRect);
// Returns true if the screen coordinate is over the map window but outside the EDG-visible area.
bool mapEdgeIsOverClippedArea(int screenX, int screenY);
} // namespace fallout
#endif /* MAP_EDGE_H */
+2 -2
View File
@@ -590,12 +590,12 @@ static void mp_run_placement_loop(PlaceFn place)
}
switch (keyCode) {
case KEY_HOME: // Home
case 327: // Home
if (gDude != nullptr) {
if (gElevation != gDude->elevation) {
mapSetElevation(gDude->elevation);
}
tileSetCenter(gDude->tile, TILE_SET_CENTER_REFRESH_WINDOW);
tileSetCenter(gDude->tile, 1);
}
break;
case KEY_ARROW_UP:

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