Compare commits

...
Author SHA1 Message Date
phobos2077 aa69e343dc mapper: obj_load_text corrections 2026-05-11 13:44:00 +02:00
phobos2077 69cc853475 mapper: obj_load_text (WIP) 2026-05-11 00:13:33 +02:00
phobos2077 331bf79ec0 mapper: save map to TXT 2026-05-10 22:56:09 +02:00
phobos2077 456f44b2a3 mapper: map_save_text initial impl 2026-05-10 17:24:02 +02:00
github-actions[bot] 8d8b19e979 chore: auto-format with clang-format 2026-05-10 01:40:28 +00:00
phobos2077 80bd1b2c8b mapper: implement debug.show_tile_num, Mark Exit Grid, toolbar name drawing fix and refactoring 2026-05-10 03:38:30 +02:00
phobos2077 0804ccac87 mapper: corrected Delete Spatial, minor fixes 2026-05-10 02:50:13 +02:00
phobos2077 4ec9f88986 mapper: edit_mapper inacuracies and tidying up some code 2026-05-10 01:55:39 +02:00
phobos2077 491384522f mapper: fix playmode and cleanup discrepancy 2026-05-09 23:55:05 +02:00
phobos2077 49f4376e89 mapper: save map corrections + some stubs 2026-05-09 23:05:47 +02:00
phobos2077 f4688409fb Mapper: fixed playmode, added some mapper features 2026-05-09 18:32:45 +02:00
Mike Klaas ab2a09610d Party members can barter (#436)
Very simple party barter support. Builds upon the loot screen so is very little code:

    Switch party members with left/right arrow keys
    When leaving barter with items on the table (canceling), all items go to the PC's inventory
    No special handling of money (yet)

The mod pins the player's money stack on top of the inventory list for all party members. That's nice, but also feels a little odd, but likely just because I'm used to the money stack being stuck at bottom. We can iterate on that.

Adding an interface will require art. I considered enabling mouse use triggered by clicking on the avatar which would be quite simple.
2026-05-07 06:02:37 +00:00
Mike Klaas cef24823f4 Speed up Fallout1 .dat reader (#439)
* Speed up Fallout1 .dat reader

It was reallocating a buffer on a loop, which caused large files to take minutes.  Fixing this reduced it to a reasonable time, but still ~120s for the entire Fallout1 master.dat.

Also, bump the F2 gzip buffer to 4kB. It was only 400 bytes
2026-05-06 13:36:11 -07:00
Mike Klaas 63d79e4bfd Fix high pitch voiced dialog (#440)
In 8b2ead8a2b, soundLoad started honoring AudioFileInfo.channels, but the ACM decoder path historically only propagated sample rate.  This changes playback in a way that caused the bug.

Verified by talking to good ol' Sulik
2026-05-05 22:55:53 -07:00
Vlad Kandgithub-actions[bot] e42d8021c1 WIP Mapper implementation (#438)
* Add mapper CMakeTarget, tool for mapping function names to originals, load/save toolbar & update_art implemented

* edit_mapper function + stubs

* Rename exe to mapper-ce

* load_lbm_to_buf

* Add comments for read/write functions in db.h

* load_dialog, save_dialog, save_as, info_dialog and some other functions

* Fix LBM loading

* Fix mouse input not working on initial empty map, changed error in partyMemberRecoverLoadInstance to print to log, matching vanilla

* mapper.cc: basic hi-res support, NULL->nullptr

* load_lbm_to_buf rewrite, print_toolbar_name background fix

* Stubs for enter/exit playmode, art slot indexes fix, map_scr_toggle_hexes

* Fix memory corruption on screen_width > 640, fix various UI offset bugs

* mapper.cc: UI code style, toggle button fixes, rotation keys, edit button placeholders, PAGEUP key fix

* Elevation display fix, object type switching

* Spatial script placement and display, basic object selection

* Fixed dragging objects, block object showing, add all missing cases in edit_mapper with stubs, move all keys codes to constants

* chore: auto-format with clang-format

* Fix non-win builds

* Add stub calls from edit_mapper, fix objects being incorrectly deleted when unselected, fix tile number display

* Fix compile on Linux

* Attempt to fix iOS signing error

* Placing of objects and tiles, F12 to erase map, bug fixes

* Fixed block object toggling logic and add missing switch cases to edit_mapper

* Object editing added, 'p' to scroll palette fixed

* Add new files to CMakeLists

* Attempt to fix some colors + alignment in critter edit window

* chore: auto-format with clang-format

* Linux build fix attempt

* Critter inventory editing

* Vanilla grid-based inventory item picker

* Review fixes

* More review fixes and const correctness

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-05 18:47:16 +00:00
8254c758fe HOOK_INVENWIELD, HOOK_CANUSEWEAPON, HOOK_ADJUSTFID (#437)
* Add support for new talking heads

Add [Heads] config section in ddraw.ini that maps critter PIDs to talking
head indices in art\heads\heads.lst. When a dialog script starts with
headId=-1, the engine now checks the critter proto's headFid first, then
falls back to the [Heads] mapping. This lets mods add talking heads
without patching dialog scripts.

Includes Cassidy mapping (PID 16777305 → head index 13) for use with
cassidy_head.dat mod.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Implement HOOK_INVENWIELD, HOOK_CANUSEWEAPON and fix HOOK_ADJUSTFID

Wire the sfall hooks that npc_armor.mod relies on so party-member sprites
actually change when armor is equipped:

- HOOK_INVENWIELD fires from inventoryEquipFunc/inventoryUnequipFunc with
  (critter, item, slot, isWield). Script may veto by returning 0. Slot
  values mirror interpreter_extra.cc: WORN=0, RIGHT_HAND=1, LEFT_HAND=2.
- HOOK_CANUSEWEAPON fires from _ai_search_inven_weap so the mod's
  weapon-anim restriction can veto AI weapon picks.
- HOOK_ADJUSTFID arg list corrected to match sfall (single currFid arg,
  critter resolved via dude_obj).
- ProgramValue::isEmpty no longer treats string values as empty — sfall
  scripts like npc_armor's `while (sect.PID)` loop depend on a non-empty
  string being truthy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* enum class

* chore: auto-format with clang-format

* bad merge

* PR feedback

* restore isEmpty() to vanilla while fixing bug

* fixes

* minor fixes

* extra

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: tectiv3 <tectiv3@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-04 21:36:50 -07:00
61 changed files with 6270 additions and 917 deletions
+79 -4
View File
@@ -38,7 +38,9 @@ endif()
# Git Info
include("cmake/gitver.cmake")
target_sources(${EXECUTABLE_NAME} PUBLIC
# Shared engine sources — used by both game and mapper targets.
# Excludes main.cc/main.h (game-only entry logic).
set(FALLOUT_ENGINE_SOURCES
"src/actions.cc"
"src/actions.h"
"src/animation.cc"
@@ -154,8 +156,6 @@ target_sources(${EXECUTABLE_NAME} PUBLIC
"src/lips.h"
"src/loadsave.cc"
"src/loadsave.h"
"src/main.cc"
"src/main.h"
"src/mainmenu.cc"
"src/mainmenu.h"
"src/map_defs.h"
@@ -268,7 +268,7 @@ target_sources(${EXECUTABLE_NAME} PUBLIC
"src/xfile.h"
)
target_sources(${EXECUTABLE_NAME} PUBLIC
set(FALLOUT_PLATFORM_SOURCES
"src/audio_engine.cc"
"src/audio_engine.h"
"src/delay.cc"
@@ -316,6 +316,14 @@ target_sources(${EXECUTABLE_NAME} PUBLIC
"third_party/lodepng/lodepng.cpp"
)
target_sources(${EXECUTABLE_NAME} PUBLIC
${FALLOUT_ENGINE_SOURCES}
"src/main.cc"
"src/main.h"
)
target_sources(${EXECUTABLE_NAME} PUBLIC ${FALLOUT_PLATFORM_SOURCES})
target_include_directories(${EXECUTABLE_NAME} PUBLIC "third_party/lodepng")
set(FALLOUT_HAVE_STB_VORBIS OFF)
@@ -561,6 +569,73 @@ if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
endif()
endif()
if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
if(WIN32)
add_executable(mapper-ce WIN32)
else()
add_executable(mapper-ce)
endif()
target_sources(mapper-ce PUBLIC
${FALLOUT_ENGINE_SOURCES}
${FALLOUT_PLATFORM_SOURCES}
"src/mapper/map_func.cc"
"src/mapper/map_func.h"
"src/mapper/mapper.cc"
"src/mapper/mapper.h"
"src/mapper/mp_instance.cc"
"src/mapper/mp_instance.h"
"src/mapper/mp_proto.cc"
"src/mapper/mp_proto.h"
"src/mapper/mp_scrpt.cc"
"src/mapper/mp_scrpt.h"
"src/mapper/mp_targt.cc"
"src/mapper/mp_targt.h"
"src/mapper/mp_text.cc"
"src/mapper/mp_text.h"
)
target_compile_definitions(mapper-ce PUBLIC FALLOUT_MAPPER)
if(WIN32)
target_compile_definitions(mapper-ce PUBLIC
_CRT_SECURE_NO_WARNINGS
_CRT_NONSTDC_NO_WARNINGS
NOMINMAX
WIN32_LEAN_AND_MEAN
_STATIC_CPPLIB
)
target_link_libraries(mapper-ce
winmm
debug libcpmtd
optimized libcpmt
)
target_sources(mapper-ce PUBLIC
"os/windows/fallout2-ce.rc"
)
endif()
if(APPLE)
target_link_libraries(mapper-ce "-framework CoreFoundation")
set_target_properties(mapper-ce PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
)
endif()
target_include_directories(mapper-ce PUBLIC "third_party/lodepng")
target_include_directories(mapper-ce PRIVATE "src")
target_include_directories(mapper-ce PRIVATE ${ZLIB_INCLUDE_DIRS})
target_include_directories(mapper-ce PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(mapper-ce
fpattern::fpattern
fpattern_windows::fpattern_windows
${ZLIB_LIBRARIES}
${SDL2_LIBRARIES}
)
endif()
if(APPLE)
if(IOS)
install(TARGETS ${EXECUTABLE_NAME} DESTINATION "Payload")
+3 -3
View File
@@ -101,8 +101,8 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| Steal | `HOOK_STEAL` | 🚫 | Et tu |
| WithinPerception | `HOOK_WITHINPERCEPTION` | ✅ | - |
| InventoryMove | `HOOK_INVENTORYMOVE` | ✅ | - |
| InvenWield | `HOOK_INVENWIELD` | 🚫 | - |
| AdjustFID | `HOOK_ADJUSTFID` | 🚫 | - |
| InvenWield | `HOOK_INVENWIELD` | | - |
| AdjustFID | `HOOK_ADJUSTFID` | | Second hook arg currently matches the first because CE has no internal FID modifiers like Hero Appearance. |
| CombatTurn | `HOOK_COMBATTURN` | ✅ | - |
| StdProcedure | `HOOK_STDPROCEDURE` | ✅ | - |
| StdProcedureEnd | `HOOK_STDPROCEDURE_END` | ✅ | - |
@@ -124,5 +124,5 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| AdjustRads | `HOOK_ADJUSTRADS` | 🚫 | (maybe) |
| RollCheck | `HOOK_ROLLCHECK` | 🚫 | - |
| BestWeapon | `HOOK_BESTWEAPON` | 🚫 | - |
| CanUseWeapon | `HOOK_CANUSEWEAPON` | 🚫 | - |
| CanUseWeapon | `HOOK_CANUSEWEAPON` | | - |
| BuildSfxWeapon | `HOOK_BUILDSFXWEAPON` | 🚫 | - |
+4
View File
@@ -22,6 +22,10 @@ save_text_maps=0
show_pid_numbers=0
sort_script_list=0
use_art_not_protos=0
; Allows to load a certain map right away after loading the mapper.
map=denbus2
; Allows to disable grid item picker and use a simpler list-based picker.
use_grid_item_picker=1
[preferences]
brightness=1.000000
+89 -1
View File
@@ -355,6 +355,14 @@ int artIsObjectTypeHidden(int objectType)
return objectType >= OBJ_TYPE_ITEM && objectType < OBJ_TYPE_COUNT ? gArtListDescriptions[objectType].flags & 1 : 0;
}
// 0x409DF0
void artToggleObjectTypeHidden(int objectType)
{
if (objectType >= 0 && objectType < OBJ_TYPE_COUNT) {
gArtListDescriptions[objectType].flags ^= 1;
}
}
// 0x418F7C
int artGetFidgetCount(int headFid)
{
@@ -439,7 +447,87 @@ void artRender(int fid, unsigned char* dest, int width, int height, int pitch)
// mapper2.exe: 0x40A03C
int art_list_str(int fid, char* name)
{
// TODO: Incomplete.
if (fid == -1 || name == nullptr) {
return -1;
}
int objectType = (fid & 0xF000000) >> 24;
int index = fid & 0xFFF;
const char* typeName = artGetObjectTypeName(objectType);
if (typeName == nullptr) {
return -1;
}
char path[260];
snprintf(path, sizeof(path), "%s%s%s\\%s.lst", _cd_path_base, "art\\", typeName, typeName);
File* stream = fileOpen(path, "rt");
if (stream == nullptr) {
return -1;
}
char buffer[260];
int line = 0;
bool found = false;
while (fileReadString(buffer, sizeof(buffer), stream) != nullptr) {
if (line == index) {
char* p = buffer;
while (*p) {
if (*p == ' ' || *p == '\n') {
*p = '\0';
break;
}
p++;
}
char* dst = name;
const char* src = buffer;
do {
*dst++ = *src++;
*dst++ = *src++;
} while (dst[-2] != '\0' || dst[-1] != '\0');
found = true;
break;
}
line++;
}
fileClose(stream);
return found ? 0 : -1;
}
// art_list_index
int artListIndex(int objectType, const char* name)
{
if (objectType < 0 || objectType >= OBJ_TYPE_COUNT) return -1;
if (gArtListDescriptions[objectType].fileNames == nullptr) return -1;
char upperName[13] = { 0 };
strncpy(upperName, name, 12);
upperName[12] = '\0';
compat_strupr(upperName);
int length = gArtListDescriptions[objectType].fileNamesLength;
const char* fileNames = gArtListDescriptions[objectType].fileNames;
for (int index = 0; index < length; index++) {
const char* entry = fileNames + index * 13;
char upperEntry[13];
strncpy(upperEntry, entry, 12);
upperEntry[12] = '\0';
compat_strupr(upperEntry);
char* p = upperEntry;
while (*p && ((*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_'))
p++;
*p = '\0';
if (strcmp(upperEntry, upperName) == 0) {
return index;
}
}
return -1;
}
+2
View File
@@ -122,6 +122,7 @@ void artReset();
void artExit();
char* artGetObjectTypeName(int objectType);
int artIsObjectTypeHidden(int objectType);
void artToggleObjectTypeHidden(int objectType);
int artGetFidgetCount(int headFid);
void artRender(int fid, unsigned char* dest, int width, int height, int pitch);
int art_list_str(int fid, char* name);
@@ -150,6 +151,7 @@ int _art_alias_num(int index);
int artCritterFidShouldRun(int fid);
int artAliasFid(int fid);
int buildFid(int objectType, int frmId, int animType, int weaponCode, int rotation);
int artListIndex(int objectType, const char* name);
Art* artLoad(const char* path);
int artRead(const char* path, unsigned char* data);
int artWrite(const char* path, unsigned char* data);
-1
View File
@@ -264,7 +264,6 @@ int audioOpen(const char* fname, AudioFileInfo* info, bool* isMemoryBackedPtr)
if (info != nullptr) {
info->sampleRate = audioFile->sampleRate;
info->channels = audioFile->channels;
info->bitsPerSample = audioFile->bitsPerSample;
}
if (isMemoryBackedPtr != nullptr) {
-1
View File
@@ -100,7 +100,6 @@ int audioFileOpen(const char* fname, AudioFileInfo* info, bool* isMemoryBackedPt
audioFile->fileSize *= 2;
if (info != nullptr) {
info->sampleRate = audioFile->sampleRate;
info->channels = audioFile->channels;
info->bitsPerSample = 16;
}
if (isMemoryBackedPtr != nullptr) {
+24 -14
View File
@@ -1972,31 +1972,41 @@ static Object* _ai_best_weapon(Object* attacker, Object* weapon1, Object* weapon
// 0x4298EC
static bool _ai_can_use_weapon(Object* critter, Object* weapon, int hitMode)
{
bool result = true;
int damageFlags = critter->data.critter.combat.results;
if ((damageFlags & DAM_CRIP_ARM_LEFT) != 0 && (damageFlags & DAM_CRIP_ARM_RIGHT) != 0) {
return false;
result = false;
}
if ((damageFlags & DAM_CRIP_ARM_ANY) != 0 && weaponIsTwoHanded(weapon)) {
return false;
if (result && (damageFlags & DAM_CRIP_ARM_ANY) != 0 && weaponIsTwoHanded(weapon)) {
result = false;
}
int rotation = critter->rotation + 1;
int animationCode = weaponGetAnimationCode(weapon);
int weaponAnimationCode = weaponGetAnimationForHitMode(weapon, hitMode);
int fid = buildFid(OBJ_TYPE_CRITTER, critter->fid & 0xFFF, weaponAnimationCode, animationCode, rotation);
if (!artExists(fid)) {
return false;
if (result) {
int rotation = critter->rotation + 1;
int animationCode = weaponGetAnimationCode(weapon);
int weaponAnimationCode = weaponGetAnimationForHitMode(weapon, hitMode);
int fid = buildFid(OBJ_TYPE_CRITTER, critter->fid & 0xFFF, weaponAnimationCode, animationCode, rotation);
if (!artExists(fid)) {
result = false;
}
}
int skill = weaponGetSkillForHitMode(weapon, hitMode);
AiPacket* ai = aiGetPacket(critter);
if (skillGetValue(critter, skill) < ai->min_to_hit) {
return false;
if (result) {
int skill = weaponGetSkillForHitMode(weapon, hitMode);
if (skillGetValue(critter, skill) < ai->min_to_hit) {
result = false;
}
}
int attackType = weaponGetAttackTypeForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY);
return _caiHasWeapPrefType(ai, attackType) != 0;
if (result) {
int attackType = weaponGetAttackTypeForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY);
result = _caiHasWeapPrefType(ai, attackType) != 0;
}
return scriptHooks_CanUseWeapon(result, critter, weapon, hitMode);
}
// 0x4299A0
+48
View File
@@ -29,34 +29,82 @@ long fileTell(File* stream);
void fileRewind(File* stream);
int fileEof(File* stream);
int fileReadUInt8(File* stream, unsigned char* valuePtr);
// Reads a 16-bit big-endian integer from stream and stores it in host byte order.
int fileReadInt16(File* stream, short* valuePtr);
// Reads a 16-bit big-endian unsigned integer from stream and stores it in host byte order.
int fileReadUInt16(File* stream, unsigned short* valuePtr);
// Reads a 32-bit big-endian integer from stream and stores it in host byte order.
int fileReadInt32(File* stream, int* valuePtr);
// Reads a 32-bit big-endian unsigned integer from stream and stores it in host byte order.
int fileReadUInt32(File* stream, unsigned int* valuePtr);
// Reads a 32-bit big-endian integer from stream and stores it in host byte order (alias).
int _db_freadInt(File* stream, int* valuePtr);
// Reads a 32-bit big-endian floating-point value from stream and stores it in host byte order.
int fileReadFloat(File* stream, float* valuePtr);
// Reads a 32-bit big-endian integer from stream and stores the boolean equivalent in host byte order.
int fileReadBool(File* stream, bool* valuePtr);
int fileWriteUInt8(File* stream, unsigned char value);
// Writes a 16-bit integer to stream in big-endian byte order.
int fileWriteInt16(File* stream, short value);
// Writes a 16-bit unsigned integer to stream in big-endian byte order.
int fileWriteUInt16(File* stream, unsigned short value);
// Writes a 32-bit integer to stream in big-endian byte order.
int fileWriteInt32(File* stream, int value);
// Writes a 32-bit integer to stream in big-endian byte order (alias).
int _db_fwriteLong(File* stream, int value);
// Writes a 32-bit unsigned integer to stream in big-endian byte order.
int fileWriteUInt32(File* stream, unsigned int value);
// Writes a 32-bit floating-point value to stream in big-endian byte order.
int fileWriteFloat(File* stream, float value);
// Writes a boolean value to stream as a 32-bit big-endian integer (1 or 0).
int fileWriteBool(File* stream, bool value);
int fileReadUInt8List(File* stream, unsigned char* arr, int count);
int fileReadFixedLengthString(File* stream, char* string, int length);
// Reads a list of 16-bit big-endian integers from stream into arr (in host byte order).
int fileReadInt16List(File* stream, short* arr, int count);
// Reads a list of 16-bit big-endian unsigned integers from stream into arr (in host byte order).
int fileReadUInt16List(File* stream, unsigned short* arr, int count);
// Reads a list of 32-bit big-endian integers from stream into arr (in host byte order).
int fileReadInt32List(File* stream, int* arr, int count);
// Reads a list of 32-bit big-endian integers from stream into arr (in host byte order, alias).
int _db_freadIntCount(File* stream, int* arr, int count);
// Reads a list of 32-bit big-endian unsigned integers from stream into arr (in host byte order).
int fileReadUInt32List(File* stream, unsigned int* arr, int count);
int fileWriteUInt8List(File* stream, unsigned char* arr, int count);
int fileWriteFixedLengthString(File* stream, char* string, int length);
// Writes a list of 16-bit integers to stream in big-endian byte order.
int fileWriteInt16List(File* stream, short* arr, int count);
// Writes a list of 16-bit unsigned integers to stream in big-endian byte order.
int fileWriteUInt16List(File* stream, unsigned short* arr, int count);
// Writes a list of 32-bit integers to stream in big-endian byte order.
int fileWriteInt32List(File* stream, int* arr, int count);
// Writes a list of 32-bit integers to stream in big-endian byte order (alias).
int _db_fwriteLongCount(File* stream, int* arr, int count);
// Writes a list of 32-bit unsigned integers to stream in big-endian byte order.
int fileWriteUInt32List(File* stream, unsigned int* arr, int count);
int fileNameListInit(const char* pattern, char*** fileNames);
void fileNameListFree(char*** fileNames, int unused);
+1 -1
View File
@@ -14,7 +14,7 @@
namespace fallout {
// The size of decompression buffer for reading compressed [DFile]s.
#define DFILE_DECOMPRESSION_BUFFER_SIZE (0x400)
#define DFILE_DECOMPRESSION_BUFFER_SIZE (0x1000)
// Specifies that [DFile] has unget character.
//
+1 -2
View File
@@ -86,7 +86,6 @@ namespace fallout {
#define SPLASH_HEIGHT (480)
#define SPLASH_COUNT (10)
static int gameLoadGlobalVars();
static int gameTakeScreenshot(int width, int height, unsigned char* buffer, unsigned char* palette);
static void gameFreeGlobalVars();
static bool tryLoadBaseCEModAtPath(const char* path, bool* found, bool* openFailed);
@@ -1016,7 +1015,7 @@ int gameSetGlobalVar(int var, int value)
// game_load_info
// 0x443CC8
static int gameLoadGlobalVars()
int gameLoadGlobalVars()
{
if (globalVarsRead("data\\vault13.gam", "GAME_GLOBAL_VARS:", &gGameGlobalVarsLength, &gGameGlobalVars) != 0) {
return -1;
+1
View File
@@ -47,6 +47,7 @@ int gameRequestState(int newGameState);
void gameUpdateState();
int showQuitConfirmationDialog();
int gameLoadGlobalVars();
int gameShowDeathDialog(const char* message);
void gameHandleSkilldexResult(int rc);
void* gameGetGlobalPointer(int var);
+23 -2
View File
@@ -313,7 +313,6 @@ Object* gGameMouseHexCursor;
// 0x596C74
static Object* gGameMousePointedObject;
static int _gmouse_get_click_to_scroll();
static void _gmouse_3d_enable_modes();
static int gameMouseSetBouncingCursorFid(int fid);
static int gameMouseRenderAccuracy(const char* string, int color);
@@ -455,6 +454,15 @@ int _gmouse_get_click_to_scroll()
return _gmouse_click_to_scroll;
}
// gmouse_set_click_to_scroll
void _gmouse_set_click_to_scroll(int value)
{
if (value != _gmouse_click_to_scroll) {
_gmouse_click_to_scroll = value;
_gmouse_clicked_on_edge = false;
}
}
// 0x44B54C
int _gmouse_is_scrolling()
{
@@ -2279,7 +2287,7 @@ int _gmouse_3d_move_to(int x, int y, int elevation, Rect* rect)
int fid = gGameMouseBouncingCursor->fid;
if (FID_TYPE(fid) == OBJ_TYPE_TILE) {
int squareTile = squareTileFromScreenXY(x, y, elevation);
if (squareTile == -1) {
if (squareTile != -1) {
tile = HEX_GRID_WIDTH * (2 * (squareTile / SQUARE_GRID_WIDTH) + 1) + 2 * (squareTile % SQUARE_GRID_WIDTH) + 1;
x1 = -8;
y1 = 13;
@@ -2383,6 +2391,19 @@ int gameMouseHandleScrolling(int x, int y, int cursor)
flags |= SCROLLABLE_S;
}
// Click-to-scroll mode (mapper Alt-Z): only scroll while the left button is held at an
// edge. Track this in _gmouse_clicked_on_edge so callers can react (e.g. suppress the
// regular 3D cursor update while an edge-click scroll is active).
// TODO: reconcile with other site where _gmouse_clicked_on_edge is set to true
if (_gmouse_click_to_scroll) {
bool atEdge = flags != 0;
bool leftHeld = (mouseGetEvent() & MOUSE_EVENT_LEFT_BUTTON_REPEAT) != 0;
_gmouse_clicked_on_edge = atEdge && leftHeld;
if (!_gmouse_clicked_on_edge) {
return -1;
}
}
int dx = 0;
int dy = 0;
+2
View File
@@ -82,6 +82,8 @@ void _gmouse_enable_scrolling();
void _gmouse_disable_scrolling();
bool gmouse_scrolling_is_enabled();
int _gmouse_is_scrolling();
int _gmouse_get_click_to_scroll();
void _gmouse_set_click_to_scroll(int value);
void gameMouseRefresh();
void _gmouse_handle_event(int mouseX, int mouseY, int mouseState);
int gameMouseSetCursor(int cursor);
+151 -4
View File
@@ -1,12 +1,13 @@
#include "graph_lib.h"
#include <string.h>
#include <algorithm>
#include <cstring>
#include "color.h"
#include "db.h"
#include "debug.h"
#include "memory.h"
#include "palette.h"
namespace fallout {
@@ -53,10 +54,156 @@ unsigned char HighRGB(unsigned char color)
}
// 0x44ED98
int load_lbm_to_buf(const char* path, unsigned char* buffer, int a3, int a4, int a5, int a6, int a7)
// Loads LBM image file into a dstBuffer.
// Returns image width on success, -1 on error.
int load_lbm_to_buf(const char* path, unsigned char* dstBuffer, int xMin, int yMin, int xMax, int yMax)
{
// TODO: Incomplete.
File* stream = fileOpen(path, "rb");
if (stream == nullptr) {
debugPrint("load_lbm_to_buf(%s): fileOpen failed\n", path);
return -1;
}
unsigned char form[4];
unsigned int formSize;
unsigned char ilbm[4];
if (fileRead(form, 1, 4, stream) != 4
|| memcmp(form, "FORM", 4) != 0
|| fileReadUInt32(stream, &formSize) == -1
|| fileRead(ilbm, 1, 4, stream) != 4) {
debugPrint("load_lbm_to_buf(%s): incorrect LBM header\n", path);
fileClose(stream);
return -1;
}
int imgWidth = 0, imgHeight = 0;
// NOTE: The original ASM's sliding-window scanner consumes the BMHD
// chunk size as a 4-byte skip, reads only the 2-byte width, and then
// lets the scanner drift through the remaining BMHD fields (height,
// nPlanes, masking, comp, etc.) while searching for the next tag.
// Height and compression are therefore never stored by the original.
// The BODY decoder always runs as PackBits unconditionally.
// paletteRemap[i] = system-palette index that best matches LBM colour i.
// Built from CMAP via colorTable. Index 0 is populated but never used
// (pixel value 0 is always output as 0 — the transparent colour).
unsigned char paletteRemap[256] = {};
while (true) {
unsigned char chunkType[4];
unsigned int chunkSize;
if (fileRead(chunkType, 1, 4, stream) != 4
|| fileReadUInt32(stream, &chunkSize) == -1) {
break;
}
unsigned int aligned = (chunkSize + 1) & ~1u;
if (memcmp(chunkType, "BMHD", 4) == 0) {
// Original reads only w (2 bytes); h is read here to advance the
// file position but its value is intentionally discarded.
unsigned short w, h;
if (fileReadUInt16(stream, &w) == -1
|| fileReadUInt16(stream, &h) == -1) break;
imgWidth = w;
imgHeight = h;
// Skip every remaining BMHD field — the original never reads them.
unsigned int bmhdRemaining = aligned - 4; // already consumed w + h
if (bmhdRemaining > 0) fileSeek(stream, bmhdRemaining, SEEK_CUR);
} else if (memcmp(chunkType, "CMAP", 4) == 0) {
// The game DOES use its own palette, but it reconciles the two
// by mapping each LBM color through colorTable (RGB555 → nearest
// system index), storing the result in paletteRemap[].
int entries = static_cast<int>(std::min(chunkSize / 3u, 256u));
for (int i = 0; i < entries; i++) {
unsigned char r, g, b;
if (fileReadUInt8(stream, &r) == -1
|| fileReadUInt8(stream, &g) == -1
|| fileReadUInt8(stream, &b) == -1) {
fileClose(stream);
return -1;
}
int rgb555 = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3);
paletteRemap[i] = _colorTable[rgb555];
}
unsigned int cmapRemaining = aligned - static_cast<unsigned int>(entries * 3);
if (cmapRemaining > 0) fileSeek(stream, cmapRemaining, SEEK_CUR);
} else if (memcmp(chunkType, "BODY", 4) == 0) {
// Decompression, palette remapping, and clipped output in a single pass with no intermediate pixel buffer.
// Output rules:
// pixel == 0 -> write 0 unconditionally (transparent)
// pixel != 0 -> write paletteRemap[pixel]
//
// The run-length case pre-remaps the pixel value once before the repeat loop.
unsigned char* dst = dstBuffer;
int col = 0; // current column - wraps at imgWidth
int row = 0; // current row - increments on wrap
// Advance the pixel cursor by one position.
auto advance = [&]() {
if (++col == imgWidth) {
col = 0;
++row;
}
};
// Emit one pixel, respecting the clip rectangle.
// Used for literal-run pixels where the remap must occur per byte.
auto emit = [&](unsigned char raw) {
if (col >= xMin && col <= xMax && row >= yMin && row <= yMax)
*dst++ = (raw == 0) ? 0 : paletteRemap[raw];
advance();
};
// PackBits RLE — always, regardless of BMHD compression field.
bool eof = false;
unsigned char ctrl;
while (!eof && fileReadUInt8(stream, &ctrl) != -1) {
if (ctrl < 0x80) {
// Literal run: the next (ctrl + 1) bytes are distinct pixels.
int count = ctrl + 1;
for (int i = 0; i < count && !eof; i++) {
unsigned char px;
if (fileReadUInt8(stream, &px) == -1)
eof = true;
else
emit(px);
}
} else {
// Run: repeat the next single pixel (257 ctrl) times.
int count = 257 - static_cast<int>(ctrl);
unsigned char px;
if (fileReadUInt8(stream, &px) == -1) {
eof = true;
} else {
// Remap once for the whole run (mirrors var_10 in the ASM).
unsigned char mapped = (px == 0) ? 0 : paletteRemap[px];
for (int i = 0; i < count; i++) {
if (col >= xMin && col <= xMax && row >= yMin && row <= yMax)
*dst++ = mapped;
advance();
}
}
}
}
// Reaching the end of BODY data (normally or via EOF) is the
// success path.
fileClose(stream);
debugPrint("load_lbm_to_buf: loaded %dx%d OK\n", imgWidth, imgHeight);
return imgWidth;
} else {
if (aligned > 0) fileSeek(stream, aligned, SEEK_CUR);
}
}
fileClose(stream);
return -1;
}
+1 -1
View File
@@ -4,7 +4,7 @@
namespace fallout {
unsigned char HighRGB(unsigned char color);
int load_lbm_to_buf(const char* path, unsigned char* buffer, int a3, int a4, int a5, int a6, int a7);
int load_lbm_to_buf(const char* path, unsigned char* dstBuffer, int xMin, int yMin, int xMax, int yMax);
int graphCompress(unsigned char* a1, unsigned char* a2, int a3);
int graphDecompress(unsigned char* a1, unsigned char* a2, int a3);
void grayscalePaletteUpdate(int a1, int a2);
+1
View File
@@ -969,6 +969,7 @@ static void buildNormalizedQwertyKeys()
keys[SDL_SCANCODE_RIGHT] = SDL_SCANCODE_RIGHT;
keys[SDL_SCANCODE_END] = SDL_SCANCODE_END;
keys[SDL_SCANCODE_DOWN] = SDL_SCANCODE_DOWN;
keys[SDL_SCANCODE_PAGEUP] = SDL_SCANCODE_PAGEUP;
keys[SDL_SCANCODE_PAGEDOWN] = SDL_SCANCODE_PAGEDOWN;
keys[SDL_SCANCODE_INSERT] = SDL_SCANCODE_INSERT;
keys[SDL_SCANCODE_DELETE] = SDL_SCANCODE_DELETE;
+3
View File
@@ -3298,6 +3298,9 @@ bool ProgramValue::isEmpty() const
switch (opcode) {
case VALUE_TYPE_INT:
case VALUE_TYPE_STRING:
case VALUE_TYPE_DYNAMIC_STRING:
// XXX: this matches the engine, but for VALUE_TYPE_STRING, 0 is valid string index, so this
// would be wrong in that case.
return integerValue == 0;
case VALUE_TYPE_FLOAT:
return floatValue == 0.0;
+45 -20
View File
@@ -36,7 +36,9 @@
#include "reaction.h"
#include "scripts.h"
#include "settings.h"
#include "sfall_config.h"
#include "sfall_opcodes.h"
#include "sfall_script_hooks.h"
#include "skill.h"
#include "stat.h"
#include "svga.h"
@@ -121,13 +123,6 @@ typedef enum CritterState {
CRITTER_STATE_PRONE = 0x02,
} CritterState;
enum {
INVEN_TYPE_WORN = 0,
INVEN_TYPE_RIGHT_HAND = 1,
INVEN_TYPE_LEFT_HAND = 2,
INVEN_TYPE_INV_COUNT = -2,
};
typedef enum FloatingMessageType {
FLOATING_MESSAGE_TYPE_WARNING = -2,
FLOATING_MESSAGE_TYPE_COLOR_SEQUENCE = -1,
@@ -414,6 +409,15 @@ static int tileIsVisible(int tile)
// 0x45409C
static int _correctFidForRemovedItem(Object* critter, Object* item, int flags)
{
InvenSlot invenSlot = InvenSlot::Armor;
if ((flags & OBJECT_IN_RIGHT_HAND) != 0) {
invenSlot = InvenSlot::RightHand;
} else if ((flags & OBJECT_IN_LEFT_HAND) != 0) {
invenSlot = InvenSlot::LeftHand;
}
scriptHooks_InvenWield(critter, item, invenSlot, 0, 1);
if (critter == gDude) {
bool animated = !gameUiIsDisabled();
interfaceUpdateItems(animated, INTERFACE_ITEM_ACTION_DEFAULT, INTERFACE_ITEM_ACTION_DEFAULT);
@@ -3015,11 +3019,16 @@ static void opCritterGetInventoryObject(Program* program)
Object* critter = static_cast<Object*>(programStackPopPointer(program));
if (PID_TYPE(critter->pid) == OBJ_TYPE_CRITTER) {
switch (type) {
case INVEN_TYPE_WORN:
if (type == kInvenSlotInvCount) {
programStackPushInteger(program, critter->data.inventory.length);
return;
}
switch (static_cast<InvenSlot>(type)) {
case InvenSlot::Armor:
programStackPushPointer(program, critterGetArmor(critter));
break;
case INVEN_TYPE_RIGHT_HAND:
case InvenSlot::RightHand:
if (critter == gDude) {
if (interfaceGetCurrentHand() != HAND_LEFT) {
programStackPushPointer(program, critterGetItem2(critter));
@@ -3030,7 +3039,7 @@ static void opCritterGetInventoryObject(Program* program)
programStackPushPointer(program, critterGetItem2(critter));
}
break;
case INVEN_TYPE_LEFT_HAND:
case InvenSlot::LeftHand:
if (critter == gDude) {
if (interfaceGetCurrentHand() == HAND_LEFT) {
programStackPushPointer(program, critterGetItem1(critter));
@@ -3041,9 +3050,6 @@ static void opCritterGetInventoryObject(Program* program)
programStackPushPointer(program, critterGetItem1(critter));
}
break;
case INVEN_TYPE_INV_COUNT:
programStackPushInteger(program, critter->data.inventory.length);
break;
default:
scriptError("script error: %s: Error in critter_inven_obj -- wrong type!", program->name);
programStackPushInteger(program, 0);
@@ -4599,24 +4605,43 @@ static void opMoveObjectInventoryToObject(Program* program)
}
Object* oldArmor = nullptr;
Object* item2 = nullptr;
Object* oldWeapon = nullptr;
if (object1 == gDude) {
oldArmor = critterGetArmor(object1);
if (interfaceGetCurrentHand() == HAND_RIGHT) {
oldWeapon = critterGetItem2(object1);
} else {
oldWeapon = critterGetItem1(object1);
}
} else {
item2 = critterGetItem2(object1);
oldWeapon = critterGetItem2(object1);
}
if (object1 != gDude && item2 != nullptr) {
if (object1 == gDude) {
if (oldWeapon != nullptr) {
InvenSlot invenSlot = interfaceGetCurrentHand() == HAND_RIGHT
? InvenSlot::RightHand
: InvenSlot::LeftHand;
scriptHooks_InvenWield(object1, oldWeapon, invenSlot, 0, 1);
}
if (oldArmor != nullptr) {
scriptHooks_InvenWield(object1, oldArmor, InvenSlot::Armor, 0, 1);
}
} else if (oldWeapon != nullptr) {
// CE intentionally reports the NPC weapon's actual hand slot here.
// Sfall's op_move_obj_inven_to_obj HOOK_INVENWIELD path passes
// InvenSlot::Armor unconditionally for NPC weapons in this case.
int flags = 0;
if ((item2->flags & OBJECT_IN_LEFT_HAND) != 0) {
if ((oldWeapon->flags & OBJECT_IN_LEFT_HAND) != 0) {
flags |= OBJECT_IN_LEFT_HAND;
}
if ((item2->flags & OBJECT_IN_RIGHT_HAND) != 0) {
if ((oldWeapon->flags & OBJECT_IN_RIGHT_HAND) != 0) {
flags |= OBJECT_IN_RIGHT_HAND;
}
_correctFidForRemovedItem(object1, item2, flags);
_correctFidForRemovedItem(object1, oldWeapon, flags);
}
itemMoveAll(object1, object2);
+8
View File
@@ -5,6 +5,14 @@
namespace fallout {
enum class InvenSlot {
Armor = 0, // INVEN_TYPE_WORN
RightHand = 1, // INVEN_TYPE_RIGHT_HAND
LeftHand = 2, // INVEN_TYPE_LEFT_HAND
};
constexpr int kInvenSlotInvCount = -2;
void _intExtraClose_();
void _initIntExtra();
void intExtraUpdate();

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