From 1175efd379f643e75b1eccb5fb219978eff0d641 Mon Sep 17 00:00:00 2001 From: Mike Klaas Date: Sun, 10 May 2026 10:15:50 -0700 Subject: [PATCH] Implement HOOK_STEAL (#442) * Implement HOOK_STEAL --- SFALL_COMPATIBILITY.md | 2 +- sfall_testing/hooks/gl_steal.ssl | 83 ++++++++++++++++++++++++++++++++ src/inventory.cc | 33 +++++++++---- src/sfall_script_hooks.cc | 48 ++++++++++++++++++ src/sfall_script_hooks.h | 1 + src/skill.cc | 47 +++++++++++++----- src/skill.h | 8 ++- 7 files changed, 197 insertions(+), 25 deletions(-) create mode 100644 sfall_testing/hooks/gl_steal.ssl diff --git a/SFALL_COMPATIBILITY.md b/SFALL_COMPATIBILITY.md index f63fc35c..7506bd28 100644 --- a/SFALL_COMPATIBILITY.md +++ b/SFALL_COMPATIBILITY.md @@ -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` | ✅ | - | diff --git a/sfall_testing/hooks/gl_steal.ssl b/sfall_testing/hooks/gl_steal.ssl new file mode 100644 index 00000000..f413cc76 --- /dev/null +++ b/sfall_testing/hooks/gl_steal.ssl @@ -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 := "", + target_name := "", + item_name := ""; + + 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 diff --git a/src/inventory.cc b/src/inventory.cc index 5de0c0b1..86d09610 100644 --- a/src/inventory.cc +++ b/src/inventory.cc @@ -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 barterComputeTablesValue(Object* dude, Object* npc, bool offerButton = false); static std::pair 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); diff --git a/src/sfall_script_hooks.cc b/src/sfall_script_hooks.cc index 64cbd479..8aa1ed01 100644 --- a/src/sfall_script_hooks.cc +++ b/src/sfall_script_hooks.cc @@ -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. diff --git a/src/sfall_script_hooks.h b/src/sfall_script_hooks.h index 4f924501..76e23a01 100644 --- a/src/sfall_script_hooks.h +++ b/src/sfall_script_hooks.h @@ -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(); diff --git a/src/skill.cc b/src/skill.cc index feec1513..770c5a9a 100644 --- a/src/skill.cc +++ b/src/skill.cc @@ -4,6 +4,8 @@ #include #include +#include + #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(SkillStealResult::Fail)) { + return SkillStealResult::Fail; + } + + if (hookResult == static_cast(SkillStealResult::Success) || hookResult == static_cast(SkillStealResult::Caught)) { + return static_cast(hookResult); + } + int howMuch; int stealModifier = -_gStealCount + 1; @@ -1086,26 +1111,22 @@ int skillsPerformStealing(Object* thief, Object* target, Object* item, bool isPl // 571: You steal the %s. // 573: You plant the %s. messageListItem.num = isPlanting ? 573 : 571; - if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) { - return -1; + if (messageListGetItem(&gSkillsMessageList, &messageListItem)) { + snprintf(text, sizeof(text), messageListItem.text, objectGetName(item)); + displayMonitorAddMessage(text); } - 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; + if (messageListGetItem(&gSkillsMessageList, &messageListItem)) { + snprintf(text, sizeof(text), messageListItem.text, objectGetName(item)); + displayMonitorAddMessage(text); } - snprintf(text, sizeof(text), messageListItem.text, objectGetName(item)); - displayMonitorAddMessage(text); - - return 0; + return SkillStealResult::Caught; } } diff --git a/src/skill.h b/src/skill.h index 16af8f22..6eded0c5 100644 --- a/src/skill.h +++ b/src/skill.h @@ -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);