Compare commits

...
Author SHA1 Message Date
Mike Klaas 0b649c416c Implement HOOK_STEAL 2026-05-07 21:31:03 -07:00
Mike Klaas 1a50bebff1 [HRP] Enable HR dialog screen
(It's not really "hi resolution", just adds a border so it looks less funny.)

Also, picked up @NovaRain's suggestion of centering dialog on the play area if it is large enough, which matches Sfall.
2026-05-07 07:56:16 -07: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
58 changed files with 4315 additions and 880 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")
+4 -4
View File
@@ -98,11 +98,11 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| KeyPress | `HOOK_KEYPRESS` | ✅ | Third hook arg is currently `0`; CE doesn't use VK codes. |
| MouseClick | `HOOK_MOUSECLICK` | ✅ | - |
| UseSkill | `HOOK_USESKILL` | 🚫 | - |
| Steal | `HOOK_STEAL` | 🚫 | Et tu |
| Steal | `HOOK_STEAL` | | - |
| 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` | 🚫 | - |
+6
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
@@ -94,6 +98,8 @@ auto_quick_save=3
display_bonus_damage=1
;Set to 1 to get notification of karma changes in the notification window
display_karma_changes=0
;Set to 1 to use the hi-res dialog border/background at resolutions above 640x480. Requires art\intrface\HR_ALLTLK.FRM (for example from f2_res.dat)
enable_dialog_border=1
;Set to one to hide areas outside map bounds when using higher than 640x480 resolution, and to zero to disable
enable_high_resolution_stencil=1
;Set to 1 to extend the action points bar to show up to 16 AP instead of 10 (requires iface_apbar_e.frm)
+83
View File
@@ -0,0 +1,83 @@
#include "sfall.h"
#include "dik.h"
#include "lib.arrays.h"
variable steal_mode := 0;
procedure steal_mode_name(variable mode) begin
if (mode == 1) then return "force_success";
if (mode == 2) then return "caught_fail";
if (mode == 3) then return "silent_fail";
if (mode == 4) then return "xp_override";
return "vanilla";
end
procedure steal_handler begin
variable
args := get_sfall_args,
thief := args[0],
target := args[1],
item := args[2],
is_planting := args[3],
quantity := args[4],
thief_name := "<null>",
target_name := "<null>",
item_name := "<null>";
if (thief) then thief_name := obj_name(thief);
if (target) then target_name := obj_name(target);
if (item) then item_name := obj_name(item);
display_msg(string_format6("steal mode=%s thief=%s target=%s item=%s planting=%d qty=%d",
steal_mode_name(steal_mode),
thief_name,
target_name,
item_name,
is_planting,
quantity));
display_msg(string_format1("steal args=%s", debug_array_str(args)));
if (thief != dude_obj) then return;
if (steal_mode == 1) then begin
display_msg("steal forcing success");
display_msg(sprintf(mstr_skill(571 + is_planting * 2), item_name));
set_sfall_return(1);
end else if (steal_mode == 2) then begin
display_msg("steal forcing caught failure");
display_msg(sprintf(mstr_skill(570 + is_planting * 2), item_name));
set_sfall_return(0);
end else if (steal_mode == 3) then begin
display_msg("steal forcing silent failure");
set_sfall_return(2);
end else if (steal_mode == 4) then begin
display_msg("steal forcing success with xp override 77");
display_msg(sprintf(mstr_skill(571 + is_planting * 2), item_name));
set_sfall_return(1);
set_sfall_return(77);
end
end
procedure keypress_handler begin
variable
pressed := get_sfall_arg_at(0),
key := get_sfall_arg_at(1);
if (not pressed) then return;
if (key != DIK_X) then return;
steal_mode += 1;
if (steal_mode > 4) then steal_mode := 0;
display_msg(string_format1("steal mode -> %s", steal_mode_name(steal_mode)));
end
procedure start begin
if (not game_loaded) then return;
display_msg("steal manual test ready: press X to cycle vanilla / force_success / caught_fail / silent_fail / xp_override");
display_msg("open a steal screen and move an item in either direction; mode 4 should award 77 XP for a successful action");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
register_hook_proc(HOOK_STEAL, steal_handler);
end
+42
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)
{
@@ -444,6 +452,40 @@ int art_list_str(int fid, char* name)
return -1;
}
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;
}
// 0x419160
Art* artLock(int fid, CacheEntry** handlePtr)
{
+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.
//
+85 -29
View File
@@ -279,6 +279,8 @@ static int gGameDialogBackgroundWindow = -1;
// 0x518744
static int gGameDialogWindow = -1;
static bool gameDialogUseHrArt = false;
// 0x518748
static Rect _backgrndRects[8] = {
{ 126, 14, 152, 40 },
@@ -291,6 +293,37 @@ static Rect _backgrndRects[8] = {
{ 504, 40, 514, 188 },
};
static bool gameDialogShouldUseHrArt()
{
return settings.ui.enable_dialog_border
&& (screenGetWidth() > GAME_DIALOG_WINDOW_WIDTH || screenGetHeight() > GAME_DIALOG_WINDOW_HEIGHT);
}
static int gameDialogHrArtYOffset()
{
return gameDialogUseHrArt ? 5 : 0;
}
static Rect gameDialogGetBackgroundRect(int index)
{
Rect rect = _backgrndRects[index];
int yOffset = gameDialogHrArtYOffset();
rect.top += yOffset;
rect.bottom += yOffset;
return rect;
}
static int gameDialogGetBackgroundWindowY()
{
// center onplay area if large enough, else center on screen
int visibleHeight = screenGetVisibleHeight();
if (visibleHeight >= GAME_DIALOG_WINDOW_HEIGHT) {
return (visibleHeight - GAME_DIALOG_WINDOW_HEIGHT) / 2;
}
return (screenGetHeight() - GAME_DIALOG_WINDOW_HEIGHT) / 2;
}
// 0x5187C8
static bool _talk_need_to_center = true;
@@ -2494,31 +2527,35 @@ int _gdCreateHeadWindow()
int windowWidth = GAME_DIALOG_WINDOW_WIDTH;
// NOTE: Uninline.
talk_to_create_background_window();
gameDialogWindowRenderBackground();
if (talk_to_create_background_window() == -1 || gameDialogWindowRenderBackground() == -1) {
_gdDestroyHeadWindow();
return -1;
}
unsigned char* buf = windowGetBuffer(gGameDialogBackgroundWindow);
ConstBuffer2D backgroundBuf = windowGetBuffer2D(gGameDialogBackgroundWindow);
for (int index = 0; index < 8; index++) {
soundContinueAll();
Rect* rect = &(_backgrndRects[index]);
int width = rect->right - rect->left;
int height = rect->bottom - rect->top;
Rect rect = gameDialogGetBackgroundRect(index);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
_backgrndBufs[index] = (unsigned char*)internal_malloc(width * height);
if (_backgrndBufs[index] == nullptr) {
_gdDestroyHeadWindow();
return -1;
}
unsigned char* src = buf;
src += windowWidth * rect->top + rect->left;
blitBufferToBuffer(src, width, height, windowWidth, _backgrndBufs[index], width);
Buffer2D savedBackgroundBuf { _backgrndBufs[index], width, height };
blitBuffer2D(backgroundBuf, rect.left, rect.top, width, height, savedBackgroundBuf);
}
_gdialog_window_create();
if (_gdialog_window_create() == -1) {
_gdDestroyHeadWindow();
return -1;
}
gGameDialogDisplayBuffer = windowGetBuffer(gGameDialogBackgroundWindow) + windowWidth * 14 + 126;
gGameDialogDisplayBuffer = windowGetBuffer(gGameDialogBackgroundWindow) + windowWidth * (14 + gameDialogHrArtYOffset()) + 126;
// TODO: jnz at 0x447275 without cmp or test, not sure what that means.
if (false) {
@@ -2547,10 +2584,12 @@ void _gdDestroyHeadWindow()
gGameDialogBackgroundWindow = -1;
}
gameDialogUseHrArt = false;
gExpandedBarterEnabled = false;
for (int index = 0; index < 8; index++) {
internal_free(_backgrndBufs[index]);
_backgrndBufs[index] = nullptr;
}
}
@@ -4551,13 +4590,14 @@ static const char* expandedBarterFrmName()
// 0x44AAD8
static int talk_to_create_background_window()
{
gameDialogUseHrArt = false;
gExpandedBarterEnabled = settings.ui.expand_barter_window
&& screenGetHeight() >= GAME_DIALOG_WINDOW_HEIGHT + kExpandedBarterExtraHeight
&& FrmImage().lock(OBJ_TYPE_INTERFACE, expandedBarterFrmName());
int backgroundWindowX = (screenGetWidth() - GAME_DIALOG_WINDOW_WIDTH) / 2;
int effectiveBgHeight = GAME_DIALOG_WINDOW_HEIGHT + (gExpandedBarterEnabled ? kExpandedBarterExtraHeight : 0);
int backgroundWindowY = (screenGetHeight() - effectiveBgHeight) / 2;
int backgroundWindowY = gameDialogGetBackgroundWindowY();
gGameDialogBackgroundWindow = windowCreate(backgroundWindowX,
backgroundWindowY,
@@ -4577,15 +4617,29 @@ static int talk_to_create_background_window()
int gameDialogWindowRenderBackground()
{
FrmImage backgroundFrmImage;
// alltlk.frm - dialog screen background
int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 103, 0, 0, 0);
if (!backgroundFrmImage.lock(backgroundFid)) {
return -1;
if (gameDialogShouldUseHrArt()) {
if (backgroundFrmImage.lock(OBJ_TYPE_INTERFACE, "HR_ALLTLK.frm")
&& backgroundFrmImage.getWidth() >= GAME_DIALOG_WINDOW_WIDTH
&& backgroundFrmImage.getHeight() >= GAME_DIALOG_WINDOW_HEIGHT) {
gameDialogUseHrArt = true;
} else {
backgroundFrmImage.unlock();
}
}
int windowWidth = GAME_DIALOG_WINDOW_WIDTH;
unsigned char* windowBuffer = windowGetBuffer(gGameDialogBackgroundWindow);
blitBufferToBuffer(backgroundFrmImage.getData(), windowWidth, 480, windowWidth, windowBuffer, windowWidth);
if (!backgroundFrmImage.isLocked()) {
// alltlk.frm - dialog screen background
FrmId backgroundFid(OBJ_TYPE_INTERFACE, 103);
if (!backgroundFrmImage.lock(backgroundFid)) {
return -1;
}
gameDialogUseHrArt = false;
}
ConstBuffer2D backgroundFrmBuf = backgroundFrmImage.getBuffer();
Buffer2D windowBuf = windowGetBuffer2D(gGameDialogBackgroundWindow);
blitBuffer2D(backgroundFrmBuf, 0, 0, GAME_DIALOG_WINDOW_WIDTH, GAME_DIALOG_WINDOW_HEIGHT, windowBuf);
if (!_dialogue_just_started) {
windowRefresh(gGameDialogBackgroundWindow);
@@ -4734,11 +4788,13 @@ void gameDialogRenderTalkingHead(Art* headFrm, int frame)
GAME_DIALOG_WINDOW_WIDTH);
}
int yOffset = gameDialogHrArtYOffset();
Rect headRect;
headRect.left = 126;
headRect.top = 14;
headRect.top = 14 + yOffset;
headRect.right = 514;
headRect.bottom = 214;
headRect.bottom = 214 + yOffset;
unsigned char* dest = windowGetBuffer(gGameDialogBackgroundWindow);
@@ -4748,7 +4804,7 @@ void gameDialogRenderTalkingHead(Art* headFrm, int frame)
_upperHighlightFrmImage.getWidth(),
dest,
426,
15,
15 + yOffset,
GAME_DIALOG_WINDOW_WIDTH,
_light_BlendTable,
_light_GrayTable);
@@ -4759,20 +4815,20 @@ void gameDialogRenderTalkingHead(Art* headFrm, int frame)
_lowerHighlightFrmImage.getWidth(),
dest,
129,
214 - _lowerHighlightFrmImage.getHeight() - 2,
214 + yOffset - _lowerHighlightFrmImage.getHeight() - 2,
GAME_DIALOG_WINDOW_WIDTH,
_dark_BlendTable,
_dark_GrayTable);
for (int index = 0; index < 8; ++index) {
Rect* rect = &(_backgrndRects[index]);
int width = rect->right - rect->left;
Rect rect = gameDialogGetBackgroundRect(index);
int width = rect.right - rect.left;
blitBufferToBufferTrans(_backgrndBufs[index],
width,
rect->bottom - rect->top,
rect.bottom - rect.top,
width,
dest + GAME_DIALOG_WINDOW_WIDTH * rect->top + rect->left,
dest + GAME_DIALOG_WINDOW_WIDTH * rect.top + rect.left,
GAME_DIALOG_WINDOW_WIDTH);
}
+1 -1
View File
@@ -2279,7 +2279,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;
+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();
+132 -42
View File
@@ -26,6 +26,7 @@
#include "game_sound.h"
#include "input.h"
#include "interface.h"
#include "interpreter_extra.h"
#include "item.h"
#include "kb.h"
#include "light.h"
@@ -131,6 +132,9 @@ constexpr int kTradeSlotCount = 3;
#define INVENTORY_PC_BODY_VIEW_MAX_X (INVENTORY_PC_BODY_VIEW_X + INVENTORY_BODY_VIEW_WIDTH)
#define INVENTORY_PC_BODY_VIEW_MAX_Y (INVENTORY_PC_BODY_VIEW_Y + INVENTORY_BODY_VIEW_HEIGHT)
#define INVENTORY_BARTER_LEFT_BODY_VIEW_X 18
#define INVENTORY_BARTER_LEFT_BODY_VIEW_Y 25
#define INVENTORY_LOOT_RIGHT_BODY_VIEW_X 422
#define INVENTORY_LOOT_RIGHT_BODY_VIEW_Y 35
@@ -166,7 +170,6 @@ constexpr int kTradeSlotCount = 3;
#define INVENTORY_LOOT_RIGHT_SCROLLER_Y_PAD (INVENTORY_LOOT_RIGHT_SCROLLER_Y + INVENTORY_SLOT_PADDING)
#define INVENTORY_LOOT_CRITTER_TOGGLE_Y (INVENTORY_LOOT_LEFT_BODY_VIEW_Y + INVENTORY_BODY_VIEW_HEIGHT + 24)
#define INVENTORY_TRADE_LEFT_SCROLLER_X_PAD (INVENTORY_TRADE_LEFT_SCROLLER_Y + INVENTORY_SLOT_PADDING)
#define INVENTORY_TRADE_LEFT_SCROLLER_Y_PAD (INVENTORY_TRADE_LEFT_SCROLLER_Y + INVENTORY_SLOT_PADDING)
@@ -323,10 +326,11 @@ static void _adjust_fid();
static void inventoryRenderSummary();
static int _inven_from_button(int keyCode, Object** outItem, Object*** outItemSlot, Object** outOwner);
static void inventoryRenderItemDescription(const char* string);
static void inventoryDrawCenteredText(unsigned char* buffer, int pitch, int width, int x, int y, const char* text, int color);
static void inventoryExamineItem(Object* critter, Object* item);
static void inventorySetLootLeftPaneCritter(Object* critter, Object* target);
static void inventorySetLeftPaneCritter(Object* critter, Object* target, int inventoryWindowType);
static void inventoryWindowOpenContextMenu(int eventCode, int inventoryWindowType);
static InventoryMoveResult _move_inventory(Object* item, int slotIndex, Object* targetObj, bool isPlanting);
static InventoryMoveResult _move_inventory(Object* item, int slotIndex, Object* targetObj, bool isPlanting, int* stealXpOverridePtr);
static std::pair<int, int> barterComputeTablesValue(Object* dude, Object* npc, bool offerButton = false);
static std::pair<int, int> barterComputeTablesWeight(Object* dude, Object* npc);
static int barterAttemptTransaction(Object* dude, Object* offerTable, Object* npc, Object* barterTable);
@@ -357,6 +361,7 @@ static int inventoryLootGetSlotY(int slotIndex);
static bool inventoryLootMouseHitTestScroller(bool targetInventory);
static int inventoryComputeAlignedMaxOffset(int length, int visibleSlots, int scrollStep);
static int inventoryGetCenteredWindowY(int windowHeight);
static void inventoryDisplayLeftPaneCompanionName(unsigned char* windowBuffer, int windowPitch, const Rect& rect, int index);
// 0x46E6D0
static const int gSummaryStats[7] = {
@@ -844,6 +849,13 @@ void inventoryResetDude()
_inven_pid = 0x1000000;
}
// inven_set_dude
void inventorySetDude(Object* obj, int pid)
{
_inven_dude = obj;
_inven_pid = pid;
}
// TODO(CE): move to more generic location
int inventoryComputeCritterFid(Object* critter, int basePid, Object* rightHandItem, Object* leftHandItem, Object* armor, int activeHand, int anim, int rotation)
{
@@ -1392,7 +1404,7 @@ static bool _setup_inventory(int inventoryWindowType)
// Invisible button representing left character.
buttonCreateAction(gInventoryBarterBackgroundWindow,
15, 25, INVENTORY_BODY_VIEW_WIDTH, INVENTORY_BODY_VIEW_HEIGHT, 2500);
INVENTORY_BARTER_LEFT_BODY_VIEW_X, INVENTORY_BARTER_LEFT_BODY_VIEW_Y, INVENTORY_BODY_VIEW_WIDTH, INVENTORY_BODY_VIEW_HEIGHT, 2500);
// Invisible button representing right character.
buttonCreateAction(gInventoryBarterBackgroundWindow,
@@ -1858,10 +1870,8 @@ static void _display_inventory(int stackOffset, int dragSlotIndex, int inventory
snprintf(formattedText, sizeof(formattedText), "%d", inventoryWeight);
}
int width = fontGetStringWidth(formattedText);
int x = inventoryLootLayout.leftScrollerX + inventoryLootLayout.scrollerWidth / 2 - width / 2;
int y = inventoryLootLayout.leftScrollerY + inventoryLootLayout.scrollerHeight + 2;
fontDrawText(windowBuffer + pitch * y + x, formattedText, width, pitch, color);
inventoryDrawCenteredText(windowBuffer, pitch, inventoryLootLayout.scrollerWidth, inventoryLootLayout.leftScrollerX, y, formattedText, color);
fontSetCurrent(oldFont);
}
@@ -1987,10 +1997,8 @@ static void _display_target_inventory(int stackOffset, int dragSlotIndex, Invent
snprintf(formattedText, sizeof(formattedText), "%d", inventoryWeight);
}
int width = fontGetStringWidth(formattedText);
int x = inventoryLootLayout.rightScrollerX + inventoryLootLayout.scrollerWidth / 2 - width / 2;
int y = inventoryLootLayout.rightScrollerY + inventoryLootLayout.scrollerHeight + 2;
fontDrawText(windowBuffer + pitch * y + x, formattedText, width, pitch, color);
inventoryDrawCenteredText(windowBuffer, pitch, inventoryLootLayout.scrollerWidth, inventoryLootLayout.rightScrollerX, y, formattedText, color);
fontSetCurrent(oldFont);
}
@@ -2113,8 +2121,8 @@ static void _display_body(int fid, int inventoryWindowType)
rect.left = 560;
rect.top = 25;
} else {
rect.left = 15;
rect.top = 25;
rect.left = INVENTORY_BARTER_LEFT_BODY_VIEW_X;
rect.top = INVENTORY_BARTER_LEFT_BODY_VIEW_Y;
}
rect.right = rect.left + INVENTORY_BODY_VIEW_WIDTH - 1;
@@ -2135,6 +2143,8 @@ static void _display_body(int fid, int inventoryWindowType)
windowBuffer + windowPitch * (rect.top + (INVENTORY_BODY_VIEW_HEIGHT - frameHeight) / 2) + (INVENTORY_BODY_VIEW_WIDTH - frameWidth) / 2 + rect.left,
windowPitch);
inventoryDisplayLeftPaneCompanionName(windowBuffer, windowPitch, rect, index);
win = gInventoryBarterBackgroundWindow;
} else {
unsigned char* windowBuffer = windowGetBuffer(gInventoryWindow);
@@ -2189,19 +2199,8 @@ static void _display_body(int fid, int inventoryWindowType)
windowBuffer + windowPitch * (rect.top + (INVENTORY_BODY_VIEW_HEIGHT - frameHeight) / 2) + (INVENTORY_BODY_VIEW_WIDTH - frameWidth) / 2 + rect.left,
windowPitch);
// Draw party member name at bottom of left avatar in loot mode.
if (index == 0 && inventoryWindowType == INVENTORY_WINDOW_TYPE_LOOT && _stack[0] != gDude) {
const char* name = critterGetName(_stack[0]);
if (name != nullptr) {
// TODO: clean up text rendering
int oldFont = fontGetCurrent();
fontSetCurrent(101);
int nameWidth = std::min(fontGetStringWidth(name), INVENTORY_BODY_VIEW_WIDTH);
int nameX = rect.left + INVENTORY_BODY_VIEW_WIDTH / 2 - nameWidth / 2;
int nameY = rect.bottom - fontGetLineHeight() - 1;
fontDrawText(windowBuffer + windowPitch * nameY + nameX, name, INVENTORY_BODY_VIEW_WIDTH, windowPitch, _colorTable[992]);
fontSetCurrent(oldFont);
}
if (inventoryWindowType == INVENTORY_WINDOW_TYPE_LOOT) {
inventoryDisplayLeftPaneCompanionName(windowBuffer, windowPitch, rect, index);
}
win = gInventoryWindow;
@@ -2703,7 +2702,7 @@ void adjustCritterStatsOnArmorChange(Object* critter, Object* oldArmor, Object*
// 0x4716E8
static void _adjust_fid()
{
gInventoryWindowDudeFid = inventoryComputeCritterFid(_inven_dude,
int fid = inventoryComputeCritterFid(_inven_dude,
_inven_pid,
gInventoryRightHandItem,
gInventoryLeftHandItem,
@@ -2711,6 +2710,7 @@ static void _adjust_fid()
interfaceGetCurrentHand(),
0,
0);
gInventoryWindowDudeFid = scriptHooks_AdjustFid(fid, fid);
}
// 0x4717E4
@@ -2988,12 +2988,14 @@ static void inventoryRestoreEquippedFromGlobals(Object* critter)
gInventoryArmor = nullptr;
}
static void inventorySetLootLeftPaneCritter(Object* critter, Object* target)
static void inventorySetLeftPaneCritter(Object* critter, Object* target, int inventoryWindowType)
{
assert(critter != nullptr);
assert(target != nullptr);
inventoryRestoreEquippedFromGlobals(_inven_dude);
// _inven_dude can point at a nested container while browsing bags, but
// equipped items always need to be restored onto the owning critter.
inventoryRestoreEquippedFromGlobals(_stack[0]);
inventoryStripEquippedToGlobals(critter);
_inven_dude = critter;
_curr_stack = 0;
@@ -3014,8 +3016,8 @@ static void inventorySetLootLeftPaneCritter(Object* critter, Object* target)
gInventoryWindowDudeFid = buildFid(OBJ_TYPE_CRITTER, critter->fid & 0xFFF, 0, animationCode, 0);
gInventoryWindowDudeRotationTimestamp = 0;
_display_inventory(0, -1, INVENTORY_WINDOW_TYPE_LOOT);
_display_body(target->fid, INVENTORY_WINDOW_TYPE_LOOT);
_display_inventory(0, -1, inventoryWindowType);
_display_body(target->fid, inventoryWindowType);
}
// 0x471CA0
@@ -3456,13 +3458,21 @@ int inventoryEquip(Object* critter, Object* item, int hand)
// 0x472768
int inventoryEquipFunc(Object* critter, Object* item, int handIndex, bool animate)
{
int itemType = itemGetType(item);
InvenSlot invenSlot = itemType == ITEM_TYPE_ARMOR
? InvenSlot::Armor
: (handIndex == HAND_RIGHT ? InvenSlot::RightHand : InvenSlot::LeftHand);
if (!scriptHooks_InvenWield(critter, item, invenSlot, 1, 0)) {
return -1;
}
if (animate) {
if (!isoIsDisabled()) {
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
}
}
int itemType = itemGetType(item);
if (itemType == ITEM_TYPE_ARMOR) {
Object* armor = critterGetArmor(critter);
if (armor != nullptr) {
@@ -3620,6 +3630,14 @@ int inventoryUnequipFunc(Object* critter, int hand, bool animate)
item = critterGetItem1(critter);
}
// Notify scripts before mutating the OBJECT_IN_ANY_HAND flag..
if (item != nullptr) {
InvenSlot invenSlot = hand == HAND_RIGHT ? InvenSlot::RightHand : InvenSlot::LeftHand;
if (!scriptHooks_InvenWield(critter, item, invenSlot, 0, 0)) {
return -1;
}
}
if (item) {
item->flags &= ~OBJECT_IN_ANY_HAND;
}
@@ -3736,6 +3754,39 @@ static int _inven_from_button(int keyCode, Object** outItem, Object*** outItemSl
return quantity;
}
static void inventoryDrawCenteredText(unsigned char* buffer, int pitch, int width, int x, int y, const char* text, int color)
{
if (text == nullptr) {
return;
}
int oldFont = fontGetCurrent();
fontSetCurrent(101);
int textWidth = std::min(fontGetStringWidth(text), width);
int textX = x + width / 2 - textWidth / 2;
fontDrawText(buffer + pitch * y + textX, text, width, pitch, color);
fontSetCurrent(oldFont);
}
static void inventoryDisplayLeftPaneCompanionName(unsigned char* windowBuffer, int windowPitch, const Rect& rect, int index)
{
if (index != 0 || _stack[0] == gDude) {
return;
}
const char* name = critterGetName(_stack[0]);
if (name == nullptr) {
return;
}
int oldFont = fontGetCurrent();
fontSetCurrent(101);
int nameY = rect.bottom - fontGetLineHeight() - 2;
fontSetCurrent(oldFont);
inventoryDrawCenteredText(windowBuffer, windowPitch, INVENTORY_BODY_VIEW_WIDTH, rect.left, nameY, name, _colorTable[992]);
}
// Displays item description.
//
// inven_display_msg
@@ -4486,12 +4537,12 @@ int inventoryOpenLooting(Object* looter, Object* target)
} else if (keyCode == KEY_ARROW_LEFT) {
if (partyTargets.size() > 1) {
partyTargetIndex = (partyTargetIndex > 0) ? partyTargetIndex - 1 : (int)partyTargets.size() - 1;
inventorySetLootLeftPaneCritter(partyTargets[partyTargetIndex], target);
inventorySetLeftPaneCritter(partyTargets[partyTargetIndex], target, INVENTORY_WINDOW_TYPE_LOOT);
}
} else if (keyCode == KEY_ARROW_RIGHT) {
if (partyTargets.size() > 1) {
partyTargetIndex = (partyTargetIndex < (int)partyTargets.size() - 1) ? partyTargetIndex + 1 : 0;
inventorySetLootLeftPaneCritter(partyTargets[partyTargetIndex], target);
inventorySetLeftPaneCritter(partyTargets[partyTargetIndex], target, INVENTORY_WINDOW_TYPE_LOOT);
}
} else if (keyCode == KEY_ARROW_UP) {
@@ -4571,11 +4622,12 @@ int inventoryOpenLooting(Object* looter, Object* target)
_gStealSize += itemGetSize(_stack[_curr_stack]);
InventoryItem* inventoryItem = &(_pud->items[_pud->length - (slotIndex + _stack_offset[_curr_stack] + 1)]);
InventoryMoveResult rc = _move_inventory(inventoryItem->item, slotIndex, _target_stack[_target_curr_stack], true);
int stealXpOverride = -1;
InventoryMoveResult rc = _move_inventory(inventoryItem->item, slotIndex, _target_stack[_target_curr_stack], true, &stealXpOverride);
if (rc == INVENTORY_MOVE_RESULT_CAUGHT_STEALING) {
isCaughtStealing = true;
} else if (rc == INVENTORY_MOVE_RESULT_SUCCESS) {
stealingXp += stealingXpBonus;
stealingXp += stealXpOverride >= 0 ? stealXpOverride : stealingXpBonus;
stealingXpBonus += 10;
}
@@ -4595,11 +4647,12 @@ int inventoryOpenLooting(Object* looter, Object* target)
_gStealSize += itemGetSize(_stack[_curr_stack]);
InventoryItem* inventoryItem = &(_target_pud->items[_target_pud->length - (slotIndex + _target_stack_offset[_target_curr_stack] + 1)]);
InventoryMoveResult rc = _move_inventory(inventoryItem->item, slotIndex, _target_stack[_target_curr_stack], false);
int stealXpOverride = -1;
InventoryMoveResult rc = _move_inventory(inventoryItem->item, slotIndex, _target_stack[_target_curr_stack], false, &stealXpOverride);
if (rc == INVENTORY_MOVE_RESULT_CAUGHT_STEALING) {
isCaughtStealing = true;
} else if (rc == INVENTORY_MOVE_RESULT_SUCCESS) {
stealingXp += stealingXpBonus;
stealingXp += stealXpOverride >= 0 ? stealXpOverride : stealingXpBonus;
stealingXpBonus += 10;
}
@@ -4713,8 +4766,11 @@ int inventoryOpenStealing(Object* thief, Object* target)
// 0x474708
// note: this is looting and stealing, not the inventory screen
static InventoryMoveResult _move_inventory(Object* item, int slotIndex, Object* targetObj, bool isPlanting)
static InventoryMoveResult _move_inventory(Object* item, int slotIndex, Object* targetObj, bool isPlanting, int* stealXpOverridePtr)
{
assert(stealXpOverridePtr != nullptr);
*stealXpOverridePtr = -1;
bool needRefresh = true;
Rect rect;
@@ -4776,13 +4832,17 @@ static InventoryMoveResult _move_inventory(Object* item, int slotIndex, Object*
}
if (quantityToMove != -1) {
bool skipMove = false;
if (_gIsSteal && _inven_dude == gDude) {
if (skillsPerformStealing(_inven_dude, targetObj, item, true) == 0) {
SkillStealResult stealResult = skillsPerformStealing(_inven_dude, targetObj, item, quantityToMove, true, stealXpOverridePtr);
if (stealResult == SkillStealResult::caught) {
result = INVENTORY_MOVE_RESULT_CAUGHT_STEALING;
} else if (stealResult == SkillStealResult::fail) {
skipMove = true;
}
}
if (result != INVENTORY_MOVE_RESULT_CAUGHT_STEALING) {
if (!skipMove && result != INVENTORY_MOVE_RESULT_CAUGHT_STEALING) {
if (itemMove(_inven_dude, targetObj, item, quantityToMove) != -1) {
result = INVENTORY_MOVE_RESULT_SUCCESS;
} else {
@@ -4803,13 +4863,17 @@ static InventoryMoveResult _move_inventory(Object* item, int slotIndex, Object*
}
if (quantityToMove != -1) {
bool skipMove = false;
if (_gIsSteal && _inven_dude == gDude) {
if (skillsPerformStealing(_inven_dude, targetObj, item, false) == 0) {
SkillStealResult stealResult = skillsPerformStealing(_inven_dude, targetObj, item, quantityToMove, false, stealXpOverridePtr);
if (stealResult == SkillStealResult::caught) {
result = INVENTORY_MOVE_RESULT_CAUGHT_STEALING;
} else if (stealResult == SkillStealResult::fail) {
skipMove = true;
}
}
if (result != INVENTORY_MOVE_RESULT_CAUGHT_STEALING) {
if (!skipMove && result != INVENTORY_MOVE_RESULT_CAUGHT_STEALING) {
if (itemMove(targetObj, _inven_dude, item, quantityToMove) == 0) {
if ((item->flags & OBJECT_IN_RIGHT_HAND) != 0) {
targetObj->fid = buildFid(FID_TYPE(targetObj->fid), targetObj->fid & 0xFFF, FID_ANIM_TYPE(targetObj->fid), 0, targetObj->rotation + 1);
@@ -5294,7 +5358,21 @@ void barterProcessUI(int win, Object* barterer, Object* playerTable, Object* bar
_target_stack[0] = barterer;
_target_stack_offset[0] = 0;
Object* const playerObj = _inven_dude;
int savedDudeFid = gInventoryWindowDudeFid;
std::vector<Object*> partyTargets = { _inven_dude };
if (settings.qol.party_loot_and_barter) {
for (Object* pm : get_all_party_members_objects(false)) {
if (pm != gDude && pm != barterer) {
partyTargets.push_back(pm);
}
}
}
int partyTargetIndex = 0;
bool isoWasEnabled = _setup_inventory(INVENTORY_WINDOW_TYPE_TRADE);
_display_target_inventory(_target_stack_offset[_target_curr_stack], -1, _target_pud, INVENTORY_WINDOW_TYPE_TRADE);
_display_inventory(_stack_offset[0], -1, INVENTORY_WINDOW_TYPE_TRADE);
_display_body(barterer->fid, INVENTORY_WINDOW_TYPE_TRADE);
@@ -5403,6 +5481,16 @@ void barterProcessUI(int win, Object* barterer, Object* playerTable, Object* bar
_display_target_inventory(_target_stack_offset[_target_curr_stack], -1, _target_pud, INVENTORY_WINDOW_TYPE_TRADE);
windowRefresh(gInventoryWindow);
}
} else if (keyCode == KEY_ARROW_LEFT) {
if (partyTargets.size() > 1) {
partyTargetIndex = (partyTargetIndex > 0) ? partyTargetIndex - 1 : (int)partyTargets.size() - 1;
inventorySetLeftPaneCritter(partyTargets[partyTargetIndex], barterer, INVENTORY_WINDOW_TYPE_TRADE);
}
} else if (keyCode == KEY_ARROW_RIGHT) {
if (partyTargets.size() > 1) {
partyTargetIndex = (partyTargetIndex < (int)partyTargets.size() - 1) ? partyTargetIndex + 1 : 0;
inventorySetLeftPaneCritter(partyTargets[partyTargetIndex], barterer, INVENTORY_WINDOW_TYPE_TRADE);
}
} else if (keyCode >= 2500 && keyCode <= 2501) {
_container_exit(keyCode, INVENTORY_WINDOW_TYPE_TRADE);
} else {
@@ -5572,7 +5660,9 @@ void barterProcessUI(int win, Object* barterer, Object* playerTable, Object* bar
itemAdd(barterer, item1, 1);
}
gInventoryWindowDudeFid = savedDudeFid;
_exit_inventory(isoWasEnabled);
_inven_dude = playerObj;
// NOTE: Uninline.
inventoryCommonFree();

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