Compare commits

...
2 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
11 changed files with 282 additions and 46 deletions
+1 -1
View File
@@ -98,7 +98,7 @@ 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` | âś… | - |
+2
View File
@@ -98,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
+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);
}
+23 -10
View File
@@ -330,7 +330,7 @@ static void inventoryDrawCenteredText(unsigned char* buffer, int pitch, int widt
static void inventoryExamineItem(Object* critter, Object* item);
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);
@@ -4622,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;
}
@@ -4646,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;
}
@@ -4764,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;
@@ -4827,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 {
@@ -4854,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);
+1
View File
@@ -160,6 +160,7 @@ void initSettingsRegistry(bool isMapper)
SETTING(display_karma_changes);
SETTING(display_bonus_damage);
SETTING(numbers_in_dialogue);
SETTING(enable_dialog_border);
SETTING_P(auto_quick_save, clamp(0, 10));
SETTING(enable_high_resolution_stencil);
SETTING(extend_ap_bar);
+1
View File
@@ -69,6 +69,7 @@ struct UISettings {
bool display_karma_changes = false;
bool display_bonus_damage = false;
bool numbers_in_dialogue = false;
bool enable_dialog_border = true;
int auto_quick_save = 0;
bool enable_high_resolution_stencil = true;
int inventory_columns = 1;
+48
View File
@@ -324,6 +324,54 @@ int scriptHooks_AmmoCost(Object* weapon, int rounds, int ammoCost, AmmoCostHookT
return overrideAmmoCost >= 0 ? overrideAmmoCost : ammoCost;
}
/*
Runs when checking an attempt to steal or plant an item.
Critter arg0 - The thief
Obj arg1 - The target
Item arg2 - The item being stolen/planted
int arg3 - 0 when stealing, 1 when planting
int arg4 - Quantity being stolen/planted
int ret0 - Override the handler:
2 - fail without being caught
1 - success
0 - fail and get caught
-1 - use engine handler
int ret1 - Override XP gained for this action. Values below 0 are ignored.
*/
int scriptHooks_Steal(Object* thief, Object* target, Object* item, bool isPlanting, int quantity, int* xpOverride)
{
assert(thief != nullptr);
assert(target != nullptr);
assert(item != nullptr);
assert(quantity >= 0);
assert(xpOverride != nullptr);
*xpOverride = -1;
ScriptHookCall hook(HOOK_STEAL, 2, { thief, target, item, isPlanting ? 1 : 0, quantity });
hook.call();
if (hook.numReturnValues() <= 0) {
return -1;
}
if (hook.numReturnValues() > 1) {
int overrideXp = hook.getReturnValueAt(1).asInt();
if (overrideXp >= 0) {
*xpOverride = overrideXp;
}
}
int overrideResult = hook.getReturnValueAt(0).asInt();
if (overrideResult >= 0 && overrideResult <= 2) {
return overrideResult;
}
return -1;
}
/*
Runs immediately after a critter dies for any reason.
+1
View File
@@ -267,6 +267,7 @@ enum AmmoCostHookType {
bool scriptHooksRegister(Program* program, HookType hookType, int procedureIndex);
bool scriptHooks_StdProcedure(int procedureNumber, Object* self, Object* source, Object* target, int fixedParam, bool after);
int scriptHooks_AmmoCost(Object* weapon, int rounds, int ammoCost, AmmoCostHookType hookType);
int scriptHooks_Steal(Object* thief, Object* target, Object* item, bool isPlanting, int quantity, int* xpOverride);
bool scriptHooksInit();
void scriptHooksReset();
+30 -5
View File
@@ -4,6 +4,8 @@
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "actions.h"
#include "color.h"
#include "combat.h"
@@ -24,6 +26,7 @@
#include "random.h"
#include "scripts.h"
#include "settings.h"
#include "sfall_script_hooks.h"
#include "stat.h"
#include "trait.h"
@@ -1028,8 +1031,30 @@ int skillUse(Object* obj, Object* target, int skill, int skillBonus)
}
// 0x4ABBE4
int skillsPerformStealing(Object* thief, Object* target, Object* item, bool isPlanting)
SkillStealResult skillsPerformStealing(Object* thief, Object* target, Object* item, int quantity, bool isPlanting, int* xpOverride)
{
assert(thief != nullptr);
assert(target != nullptr);
assert(item != nullptr);
assert(quantity >= 0);
assert(xpOverride != nullptr);
*xpOverride = -1;
int hookXpOverride = -1;
int hookResult = scriptHooks_Steal(thief, target, item, isPlanting, quantity, &hookXpOverride);
if (hookXpOverride >= 0) {
*xpOverride = hookXpOverride;
}
if (hookResult == static_cast<int>(SkillStealResult::fail)) {
return SkillStealResult::fail;
}
if (hookResult == static_cast<int>(SkillStealResult::success) || hookResult == static_cast<int>(SkillStealResult::caught)) {
return static_cast<SkillStealResult>(hookResult);
}
int howMuch;
int stealModifier = -_gStealCount + 1;
@@ -1087,25 +1112,25 @@ int skillsPerformStealing(Object* thief, Object* target, Object* item, bool isPl
// 573: You plant the %s.
messageListItem.num = isPlanting ? 573 : 571;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
return SkillStealResult::fail;
}
snprintf(text, sizeof(text), messageListItem.text, objectGetName(item));
displayMonitorAddMessage(text);
return 1;
return SkillStealResult::success;
} else {
// 570: You're caught stealing the %s.
// 572: You're caught planting the %s.
messageListItem.num = isPlanting ? 572 : 570;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
return SkillStealResult::fail;
}
snprintf(text, sizeof(text), messageListItem.text, objectGetName(item));
displayMonitorAddMessage(text);
return 0;
return SkillStealResult::caught;
}
}
+7 -1
View File
@@ -12,6 +12,12 @@ extern int _gIsSteal;
extern int _gStealCount;
extern int _gStealSize;
enum class SkillStealResult {
caught = 0,
success = 1,
fail = 2,
};
int skillsInit();
void skillsReset();
void skillsExit();
@@ -35,7 +41,7 @@ char* skillGetDescription(int skill);
char* skillGetAttributes(int skill);
int skillGetFrmId(int skill);
int skillUse(Object* obj, Object* target, int skill, int skillBonus);
int skillsPerformStealing(Object* thief, Object* target, Object* item, bool isPlanting);
SkillStealResult skillsPerformStealing(Object* thief, Object* target, Object* item, int quantity, bool isPlanting, int* xpOverride);
int skillGetGameDifficultyModifier(int skill);
int skillUpdateLastUse(int skill);
int skillsUsageSave(File* stream);