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
18 changed files with 1532 additions and 27 deletions
+2
View File
@@ -205,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"
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

+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;
+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();
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
#ifndef FALLOUT_PROTOTYPE_OPTIONS_H_
#define FALLOUT_PROTOTYPE_OPTIONS_H_
namespace fallout {
int showPrototypeOptionsMenu(bool animated);
} // namespace fallout
#endif /* FALLOUT_PROTOTYPE_OPTIONS_H_ */
+10
View File
@@ -364,6 +364,16 @@ void sfall_kb_clear_synthetic_key_events()
syntheticKeyEvents.clear();
}
int sfall_kb_get_scancode_from_key(int key)
{
return get_scancode_from_key(key);
}
int sfall_kb_get_key_from_scancode(int sdlScanCode)
{
return get_key_from_scancode(static_cast<SDL_Scancode>(sdlScanCode));
}
int sfall_kb_handle_key_pressed(int sdlScanCode, bool pressed)
{
if (!gGameLoaded) return SDL_SCANCODE_UNKNOWN;
+3
View File
@@ -15,6 +15,9 @@ bool sfall_kb_consume_synthetic_key_event(int sdlScanCode, bool pressed);
/// Clears queued synthetic `tap_key` markers after SDL key events are discarded.
void sfall_kb_clear_synthetic_key_events();
int sfall_kb_get_scancode_from_key(int key);
int sfall_kb_get_key_from_scancode(int sdlScanCode);
int sfall_kb_handle_key_pressed(int sdlScanCode, bool pressed);
} // namespace fallout
+76 -11
View File
@@ -64,9 +64,15 @@ static void tileRenderRoof(int fid, int x, int y, Rect* rect, int light);
static void _draw_grid(int tile, int elevation, Rect* rect);
static void tileRenderFloor(int fid, int x, int y, Rect* rect);
static int _tile_make_line(int currentCenterTile, int newCenterTile, int* tiles, int tilesCapacity);
static void tileToScrollCoord(int tile, int* outX, int* outY);
static void debugLogScrollLimitBlock(int tile, int centerCoordX, int centerCoordY, int dudeCoordX, int dudeCoordY, int dx, int dy);
static void debugLogScrollObjectBlock(int tile);
static void debugLogScrollBorderBlock(int tile, int tileX, int tileY);
// 0x50E7C7 minus_4_0f
static double const dbl_50E7C7 = -4.0;
static constexpr int kScrollLimitX = 480;
static constexpr int kScrollLimitY = 400;
// 0x51D950 borderInitialized
bool gTileBorderInitialized = false;
@@ -484,6 +490,58 @@ static void tileSetBorder(int windowWidth, int windowHeight, int hexGridWidth, i
gTileBorderInitialized = true;
}
static void tileToScrollCoord(int tile, int* outX, int* outY)
{
int x = tile % gHexGridWidth;
int y = (tile / gHexGridWidth) + (x / 2);
*outY = y;
*outX = 2 * x - y;
}
static void debugLogScrollLimitBlock(int tile, int centerCoordX, int centerCoordY, int dudeCoordX, int dudeCoordY, int dx, int dy)
{
int tileCoordX;
int tileCoordY;
tileToScrollCoord(tile, &tileCoordX, &tileCoordY);
debugPrint("tileSetCenter: scroll limit blocked tile=%d center=%d dude=%d tileCoord=(%d,%d) centerCoord=(%d,%d) dudeCoord=(%d,%d) dist=(%d,%d) limit=(%d,%d)\n",
tile,
gCenterTile,
gDude != nullptr ? gDude->tile : -1,
tileCoordX,
tileCoordY,
centerCoordX,
centerCoordY,
dudeCoordX,
dudeCoordY,
dx,
dy,
kScrollLimitX,
kScrollLimitY);
}
static void debugLogScrollObjectBlock(int tile)
{
debugPrint("tileSetCenter: scroll blocker object blocked tile=%d center=%d elev=%d\n",
tile,
gCenterTile,
gElevation);
}
static void debugLogScrollBorderBlock(int tile, int tileX, int tileY)
{
debugPrint("tileSetCenter: tile border blocked tile=%d center=%d tileXY=(%d,%d) borderX=[%d,%d] borderY=[%d,%d]\n",
tile,
gCenterTile,
tileX,
tileY,
gTileBorderMinX,
gTileBorderMaxX,
gTileBorderMinY,
gTileBorderMaxY);
}
// NOTE: Collapsed.
//
// 0x4B129C
@@ -542,20 +600,25 @@ int tileSetCenter(int tile, int flags)
if ((flags & TILE_SET_CENTER_FLAG_IGNORE_SCROLL_RESTRICTIONS) == 0) {
if (gTileScrollLimitingEnabled) {
int tileScreenX;
int tileScreenY;
tileToScreenXY(tile, &tileScreenX, &tileScreenY);
int tileCoordX;
int tileCoordY;
tileToScrollCoord(tile, &tileCoordX, &tileCoordY);
int dudeScreenX;
int dudeScreenY;
tileToScreenXY(gDude->tile, &dudeScreenX, &dudeScreenY);
int dudeCoordX;
int dudeCoordY;
tileToScrollCoord(gDude->tile, &dudeCoordX, &dudeCoordY);
int dx = abs(dudeScreenX - tileScreenX);
int dy = abs(dudeScreenY - tileScreenY);
int dx = 16 * abs(tileCoordX - dudeCoordX);
int dy = 12 * abs(tileCoordY - dudeCoordY);
if (dx > abs(dudeScreenX - _tile_offx)
|| dy > abs(dudeScreenY - _tile_offy)) {
if (dx >= 480 || dy >= 400) {
if (dx >= kScrollLimitX || dy >= kScrollLimitY) {
int centerCoordX;
int centerCoordY;
tileToScrollCoord(gCenterTile, &centerCoordX, &centerCoordY);
if (16 * abs(centerCoordX - dudeCoordX) < dx
|| 12 * abs(centerCoordY - dudeCoordY) < dy) {
debugLogScrollLimitBlock(tile, centerCoordX, centerCoordY, dudeCoordX, dudeCoordY, dx, dy);
return -1;
}
}
@@ -563,6 +626,7 @@ int tileSetCenter(int tile, int flags)
if (gTileScrollBlockingEnabled && !settings.ui.ignore_map_edges) {
if (_obj_scroll_blocking_at(tile, gElevation) == 0) {
debugLogScrollObjectBlock(tile);
return -1;
}
}
@@ -573,6 +637,7 @@ int tileSetCenter(int tile, int flags)
if (gTileBorderInitialized && !settings.ui.ignore_map_edges) {
if (tile_x <= gTileBorderMinX || tile_x >= gTileBorderMaxX || tile_y <= gTileBorderMinY || tile_y >= gTileBorderMaxY) {
debugLogScrollBorderBlock(tile, tile_x, tile_y);
return -1;
}
}