From dafb7b3831c059315f7986d71e2e3789cdd88040 Mon Sep 17 00:00:00 2001 From: Mike Klaas Date: Wed, 8 Jul 2026 12:31:49 -0700 Subject: [PATCH 1/4] Add offset tracking to --scan-unimplemented (#517) * Add offset tracking to --scan-unimplemented * Add a bunch of missing opcodes * Hardcode a list of implemented hooks to avoid filtering a long list of hooks that are already done * Add tracking of specific offsets accessed in unsafe scripting so we can plan replacements Sample output ``` OFFSET write_int (0x81d1 - 0x1d1 - 465): 0x5190f8 (5345528): - ./mods/InventoryFilter.dat/scripts/gl_InvenFilter_debug.int - ./mods/InventoryFilter.dat/scripts/release_gl_InvenFilter.int 0x59e95c (5892444): - ./mods/InventoryFilter.dat/scripts/gl_InvenFilter_debug.int - ./mods/InventoryFilter.dat/scripts/release_gl_InvenFilter.int ``` Then we can corrlated it with source: ``` // 0x5190F8 curr_rot static int gInventoryWindowDudeRotation = 0; ``` * PR feedback --- src/scan_unimplemented_opcodes.h | 447 ++++++++++++++++++++++++++++++- src/scan_unimplemented_sfall.h | 147 +++++++++- src/sfall_opcodes.cc | 2 +- 3 files changed, 572 insertions(+), 24 deletions(-) diff --git a/src/scan_unimplemented_opcodes.h b/src/scan_unimplemented_opcodes.h index cca7d1db..e354587b 100644 --- a/src/scan_unimplemented_opcodes.h +++ b/src/scan_unimplemented_opcodes.h @@ -17,13 +17,57 @@ #include #include #include +#include std::map> unknown_opcodes; std::map> sus_strings; std::map> unknown_hooks; +std::map>> offset_requests; int checked_files = 0; +// right now registered hooks aren't all implemented, so this is a shortcut to list +// hooks that are implemented +bool is_implemented_hook(int hookId) +{ + switch (hookId) { + case 0: // HOOK_TOHIT + case 1: // HOOK_AFTERHITROLL + case 2: // HOOK_CALCAPCOST + case 4: // HOOK_DEATHANIM2 + case 5: // HOOK_COMBATDAMAGE + case 6: // HOOK_ONDEATH + case 8: // HOOK_USEOBJON + case 9: // HOOK_REMOVEINVENOBJ + case 10: // HOOK_BARTERPRICE + case 11: // HOOK_MOVECOST + case 16: // HOOK_ITEMDAMAGE + case 17: // HOOK_AMMOCOST + case 18: // HOOK_USEOBJ + case 19: // HOOK_KEYPRESS + case 20: // HOOK_MOUSECLICK + case 21: // HOOK_USESKILL + case 22: // HOOK_STEAL + case 23: // HOOK_WITHINPERCEPTION + case 24: // HOOK_INVENTORYMOVE + case 25: // HOOK_INVENWIELD + case 26: // HOOK_ADJUSTFID + case 27: // HOOK_COMBATTURN + case 30: // HOOK_RESTTIMER + case 31: // HOOK_GAMEMODECHANGE + case 33: // HOOK_EXPLOSIVETIMER + case 34: // HOOK_DESCRIPTIONOBJ + case 35: // HOOK_USESKILLON + case 40: // HOOK_STDPROCEDURE + case 41: // HOOK_STDPROCEDURE_END + case 43: // HOOK_ENCOUNTER + case 48: // HOOK_CANUSEWEAPON + return true; + default: + return false; + } +} + std::string get_hook_name(int hookId) { static constexpr char const* hook_names[] = { @@ -99,6 +143,343 @@ std::string get_hook_name(int hookId) return std::string(hook_names[hookId]); } +int get_offset_request_argument_count(unsigned int opcodeIndex) +{ + switch (opcodeIndex) { + case 0x156: // read_byte + case 0x157: // read_short + case 0x158: // read_int + case 0x159: // read_string + case 0x1d2: // call_offset_v0 + return 1; + case 0x1cf: // write_byte + case 0x1d0: // write_short + case 0x1d1: // write_int + case 0x1d3: // call_offset_v1 + case 0x21b: // write_string + return 2; + case 0x1d4: // call_offset_v2 + return 3; + case 0x1d5: // call_offset_v3 + return 4; + case 0x1d6: // call_offset_v4 + return 5; + case 0x1d7: // call_offset_r0 + return 1; + case 0x1d8: // call_offset_r1 + return 2; + case 0x1d9: // call_offset_r2 + return 3; + case 0x1da: // call_offset_r3 + return 4; + case 0x1db: // call_offset_r4 + return 5; + default: + return 0; + } +} + +bool offset_request_returns_value(unsigned int opcodeIndex) +{ + switch (opcodeIndex) { + case 0x156: // read_byte + case 0x157: // read_short + case 0x158: // read_int + case 0x159: // read_string + case 0x1d7: // call_offset_r0 + case 0x1d8: // call_offset_r1 + case 0x1d9: // call_offset_r2 + case 0x1da: // call_offset_r3 + case 0x1db: // call_offset_r4 + return true; + default: + return false; + } +} + +std::string offset_request_value_to_string(int value) +{ + return toHexString(static_cast(value)) + " (" + std::to_string(value) + ")"; +} + +struct ScannedStackValue { + bool isKnownInt = false; + int value = 0; +}; + +ScannedStackValue unknownStackValue() +{ + return ScannedStackValue {}; +} + +ScannedStackValue knownIntStackValue(int value) +{ + ScannedStackValue stackValue; + stackValue.isKnownInt = true; + stackValue.value = value; + return stackValue; +} + +void pop_scanned_stack_values(std::vector& stack, int count) +{ + if (count <= 0) { + return; + } + + if (stack.size() < static_cast(count)) { + stack.clear(); + return; + } + + stack.resize(stack.size() - static_cast(count)); +} + +void push_unknown_stack_value(std::vector& stack) +{ + stack.push_back(unknownStackValue()); +} + +void simulate_unary_stack_operation(std::vector& stack, ScannedStackValue (*op)(ScannedStackValue)) +{ + if (stack.empty()) { + push_unknown_stack_value(stack); + return; + } + + auto value = stack.back(); + stack.pop_back(); + stack.push_back(op(value)); +} + +void simulate_binary_stack_operation(std::vector& stack, ScannedStackValue (*op)(ScannedStackValue, ScannedStackValue)) +{ + if (stack.size() < 2) { + stack.clear(); + push_unknown_stack_value(stack); + return; + } + + const auto right = stack.back(); + stack.pop_back(); + const auto left = stack.back(); + stack.pop_back(); + stack.push_back(op(left, right)); +} + +ScannedStackValue stack_add(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt) { + return knownIntStackValue(left.value + right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_subtract(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt) { + return knownIntStackValue(left.value - right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_multiply(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt) { + return knownIntStackValue(left.value * right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_divide(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt && right.value != 0) { + return knownIntStackValue(left.value / right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_modulo(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt && right.value != 0) { + return knownIntStackValue(left.value % right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_bitwise_and(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt) { + return knownIntStackValue(left.value & right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_bitwise_or(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt) { + return knownIntStackValue(left.value | right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_bitwise_xor(ScannedStackValue left, ScannedStackValue right) +{ + if (left.isKnownInt && right.isKnownInt) { + return knownIntStackValue(left.value ^ right.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_negate(ScannedStackValue value) +{ + if (value.isKnownInt) { + return knownIntStackValue(-value.value); + } + return unknownStackValue(); +} + +ScannedStackValue stack_bitwise_not(ScannedStackValue value) +{ + if (value.isKnownInt) { + return knownIntStackValue(~value.value); + } + return unknownStackValue(); +} + +void simulate_scanned_stack_opcode( + std::vector& stack, + fallout::opcode_t opcode, + unsigned char* data, + size_t opcodePos) +{ + const unsigned int opcodeIndex = opcode & 0x3FF; + + const int offsetArgCount = get_offset_request_argument_count(opcodeIndex); + if (offsetArgCount > 0) { + pop_scanned_stack_values(stack, offsetArgCount); + if (offset_request_returns_value(opcodeIndex)) { + push_unknown_stack_value(stack); + } + return; + } + + switch (opcode) { + case VALUE_TYPE_INT: + stack.push_back(knownIntStackValue(fallout::stackReadInt32(data, opcodePos + 2))); + break; + case VALUE_TYPE_FLOAT: + case VALUE_TYPE_STRING: + case VALUE_TYPE_DYNAMIC_STRING: + case VALUE_TYPE_PTR: + push_unknown_stack_value(stack); + break; + case fallout::OPCODE_FETCH: + case fallout::OPCODE_FETCH_GLOBAL: + case fallout::OPCODE_FETCH_EXTERNAL: + pop_scanned_stack_values(stack, 1); + push_unknown_stack_value(stack); + break; + case fallout::OPCODE_STORE: + case fallout::OPCODE_STORE_GLOBAL: + case fallout::OPCODE_STORE_EXTERNAL: + pop_scanned_stack_values(stack, 2); + break; + case fallout::OPCODE_POP: + pop_scanned_stack_values(stack, 1); + break; + case fallout::OPCODE_DUP: + if (!stack.empty()) { + stack.push_back(stack.back()); + } + break; + case fallout::OPCODE_SWAP: + if (stack.size() >= 2) { + std::iter_swap(stack.end() - 1, stack.end() - 2); + } else { + stack.clear(); + } + break; + case fallout::OPCODE_ADD: + simulate_binary_stack_operation(stack, stack_add); + break; + case fallout::OPCODE_SUB: + simulate_binary_stack_operation(stack, stack_subtract); + break; + case fallout::OPCODE_MUL: + simulate_binary_stack_operation(stack, stack_multiply); + break; + case fallout::OPCODE_DIV: + simulate_binary_stack_operation(stack, stack_divide); + break; + case fallout::OPCODE_MOD: + simulate_binary_stack_operation(stack, stack_modulo); + break; + case fallout::OPCODE_BITWISE_AND: + simulate_binary_stack_operation(stack, stack_bitwise_and); + break; + case fallout::OPCODE_BITWISE_OR: + simulate_binary_stack_operation(stack, stack_bitwise_or); + break; + case fallout::OPCODE_BITWISE_XOR: + simulate_binary_stack_operation(stack, stack_bitwise_xor); + break; + case fallout::OPCODE_BITWISE_NOT: + simulate_unary_stack_operation(stack, stack_bitwise_not); + break; + case fallout::OPCODE_NEGATE: + simulate_unary_stack_operation(stack, stack_negate); + break; + case fallout::OPCODE_EQUAL: + case fallout::OPCODE_NOT_EQUAL: + case fallout::OPCODE_LESS_THAN_EQUAL: + case fallout::OPCODE_GREATER_THAN_EQUAL: + case fallout::OPCODE_LESS_THAN: + case fallout::OPCODE_GREATER_THAN: + case fallout::OPCODE_AND: + case fallout::OPCODE_OR: + pop_scanned_stack_values(stack, 2); + push_unknown_stack_value(stack); + break; + case fallout::OPCODE_NOT: + case fallout::OPCODE_FLOOR: + pop_scanned_stack_values(stack, 1); + push_unknown_stack_value(stack); + break; + case fallout::OPCODE_CALL: + pop_scanned_stack_values(stack, 1); + push_unknown_stack_value(stack); + break; + case fallout::OPCODE_CALL_AT: + case fallout::OPCODE_CALL_WHEN: + case fallout::OPCODE_SPAWN: + case fallout::OPCODE_FORK: + case fallout::OPCODE_EXEC: + stack.clear(); + break; + default: + break; + } +} + +void track_offset_request(std::string fName, fallout::opcode_t opcode, const std::vector& stack) +{ + const unsigned int opcodeIndex = opcode & 0x3FF; + const int argCount = get_offset_request_argument_count(opcodeIndex); + if (argCount == 0) { + return; + } + + std::string offset = ""; + if (stack.size() >= static_cast(argCount)) { + const auto& firstArg = stack[stack.size() - static_cast(argCount)]; + if (firstArg.isKnownInt) { + offset = offset_request_value_to_string(firstArg.value); + } + } + + offset_requests[opcode][offset].insert(fName); +} + void check_int_data( std::string fName, unsigned char* data, @@ -106,8 +487,9 @@ void check_int_data( size_t end_pos) { size_t i = start_pos; - bool isPreviousPush = false; + std::vector stack; while (i < end_pos) { + const size_t opcodePos = i; auto opcode = fallout::stackReadInt16(data, i); if (!((opcode >> 8) & 0x80)) { printf("ERROR: Wrong opcode %x in file %s at pos=0x%lx\n", opcode, fName.c_str(), i); @@ -120,11 +502,14 @@ void check_int_data( set.insert(fName); }; + track_offset_request(fName, opcode, stack); + if (opcodeIndex == 0x207) { // register_hook if (i >= 6 && fallout::stackReadInt16(data, i - 6) == 0xC001) { - auto hookProcIndex = fallout::stackReadInt32(data, i - 6 + 2); - // All hooks are unknown atm - unknown_hooks[hookProcIndex].insert(fName); + auto hookId = fallout::stackReadInt32(data, i - 6 + 2); + if (!is_implemented_hook(hookId)) { + unknown_hooks[hookId].insert(fName); + } } else { printf("ERROR: Unknown usage of register_hook in file %s at pos=0x%lx\n", fName.c_str(), i); exit(1); @@ -132,9 +517,10 @@ void check_int_data( } else if (opcodeIndex == 0x262 || opcodeIndex == 0x27d) { // register_hook_proc / register_hook_proc_spec if ( i >= 6 * 2 && fallout::stackReadInt16(data, i - 6) == 0xC001 && fallout::stackReadInt16(data, i - 6 * 2) == 0xC001) { - auto hookProcIndex = fallout::stackReadInt32(data, i - 6 * 2 + 2); - // All hooks are unknown atm - unknown_hooks[hookProcIndex].insert(fName); + auto hookId = fallout::stackReadInt32(data, i - 6 * 2 + 2); + if (!is_implemented_hook(hookId)) { + unknown_hooks[hookId].insert(fName); + } } else { printf("ERROR: Unknown usage of register_hook_proc in file %s at pos=0x%lx\n", fName.c_str(), i); exit(1); @@ -147,10 +533,9 @@ void check_int_data( if (opcodeIndex == (fallout::OPCODE_PUSH & 0x3FF)) { i += 4; - isPreviousPush = true; - } else { - isPreviousPush = false; } + + simulate_scanned_stack_opcode(stack, opcode, data, opcodePos); } }; @@ -336,6 +721,7 @@ void checkScriptsOpcodes() unknown_opcodes.clear(); sus_strings.clear(); unknown_hooks.clear(); + offset_requests.clear(); checked_files = 0; @@ -357,6 +743,21 @@ void checkScriptsOpcodes() } nameSet = std::move(updatedNames); } + for (auto& [opcode, offsetSet] : offset_requests) { + for (auto& [offset, nameSet] : offsetSet) { + std::set updatedNames; + for (const auto& name : nameSet) { + std::string trimmed = name; + + if (trimmed.rfind(folderName, 0) == 0) { // prefix match + trimmed.erase(0, folderName.length()); + } + + updatedNames.insert(std::move(trimmed)); + } + nameSet = std::move(updatedNames); + } + } for (auto& [opcode, nameSet] : sus_strings) { std::set updatedNames; for (const auto& name : nameSet) { @@ -372,7 +773,7 @@ void checkScriptsOpcodes() } } - if (unknown_opcodes.size() == 0 && sus_strings.size() == 0 && unknown_hooks.size() == 0) { + if (unknown_opcodes.size() == 0 && sus_strings.size() == 0 && unknown_hooks.size() == 0 && offset_requests.size() == 0) { printf("Everything is ok, all opcodes are known and no sus strings. Checked %i files\n", checked_files); } else { printf("\n\nChecked %i files and found those:\n", checked_files); @@ -401,6 +802,17 @@ void checkScriptsOpcodes() printf(" - %s\n", fName.c_str()); } } + for (const auto& [opcode, offsets] : offset_requests) { + printf("OFFSET %s (0x%x - 0x%x - %i):\n", + get_opcode_name(opcode).c_str(), + opcode, opcode & 0x3FF, opcode & 0x3FF); + for (const auto& [offset, files] : offsets) { + printf(" %s:\n", offset.c_str()); + for (auto fName : files) { + printf(" - %s\n", fName.c_str()); + } + } + } printf("\nSame but per-file:\n"); // TODO: Sort @@ -415,6 +827,17 @@ void checkScriptsOpcodes() files[fName].insert(oss); } } + for (const auto& [opcode, offsets] : offset_requests) { + for (const auto& [offset, offsetFiles] : offsets) { + for (auto fName : offsetFiles) { + std::string oss = "OFFSET " + get_opcode_name(opcode) + " " + offset + + " " + toHexString(opcode & 0x3FF) + + " (" + toHexString(opcode) + ")"; + + files[fName].insert(oss); + } + } + } for (auto iter : sus_strings) { for (auto fName : iter.second) { files[fName].insert(std::string("METARULE ") + iter.first); @@ -439,4 +862,4 @@ void checkScriptsOpcodes() printf("Done\n"); printf("\n\n"); -} \ No newline at end of file +} diff --git a/src/scan_unimplemented_sfall.h b/src/scan_unimplemented_sfall.h index e1457b9b..160c0d66 100644 --- a/src/scan_unimplemented_sfall.h +++ b/src/scan_unimplemented_sfall.h @@ -116,6 +116,10 @@ struct SfallOpcodeInfo { }; static struct SfallOpcodeInfo opcodeInfoArray[] = { + { 0x156, "read_byte" }, + { 0x157, "read_short" }, + { 0x158, "read_int" }, + { 0x159, "read_string" }, { 0x15a, "set_pc_base_stat" }, { 0x15b, "set_pc_extra_stat" }, { 0x15c, "get_pc_base_stat" }, @@ -124,46 +128,143 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x15f, "set_critter_extra_stat" }, { 0x160, "get_critter_base_stat" }, { 0x161, "get_critter_extra_stat" }, + { 0x162, "tap_key" }, { 0x163, "get_year" }, + { 0x164, "game_loaded" }, + { 0x165, "graphics_funcs_available" }, + { 0x166, "load_shader" }, + { 0x167, "free_shader" }, + { 0x168, "activate_shader" }, + { 0x169, "deactivate_shader" }, + { 0x16a, "set_global_script_repeat" }, + { 0x16b, "input_funcs_available" }, { 0x16c, "key_pressed" }, + { 0x16d, "set_shader_int" }, + { 0x16e, "set_shader_float" }, + { 0x16f, "set_shader_vector" }, + { 0x170, "in_world_map" }, { 0x171, "force_encounter" }, + { 0x172, "set_world_map_pos" }, + { 0x173, "get_world_map_x_pos" }, + { 0x174, "get_world_map_y_pos" }, { 0x175, "set_dm_model" }, { 0x176, "set_df_model" }, { 0x177, "set_movie_path" }, + { 0x178, "set_perk_image" }, + { 0x179, "set_perk_ranks" }, + { 0x17a, "set_perk_level" }, + { 0x17b, "set_perk_stat" }, + { 0x17c, "set_perk_stat_mag" }, + { 0x17d, "set_perk_skill1" }, + { 0x17e, "set_perk_skill1_mag" }, + { 0x17f, "set_perk_type" }, + { 0x180, "set_perk_skill2" }, + { 0x181, "set_perk_skill2_mag" }, + { 0x182, "set_perk_str" }, + { 0x183, "set_perk_per" }, + { 0x184, "set_perk_end" }, + { 0x185, "set_perk_chr" }, + { 0x186, "set_perk_int" }, + { 0x187, "set_perk_agl" }, + { 0x188, "set_perk_lck" }, { 0x189, "set_perk_name" }, { 0x18a, "set_perk_desc" }, { 0x18b, "set_pipboy_available" }, + { 0x18c, "get_kill_counter" }, + { 0x18d, "mod_kill_counter" }, + { 0x18e, "get_perk_owed" }, + { 0x18f, "set_perk_owed" }, { 0x190, "get_perk_available" }, { 0x191, "get_critter_current_ap" }, { 0x192, "set_critter_current_ap" }, + { 0x193, "active_hand" }, + { 0x194, "toggle_active_hand" }, { 0x195, "set_weapon_knockback" }, { 0x196, "set_target_knockback" }, { 0x197, "set_attacker_knockback" }, { 0x198, "remove_weapon_knockback" }, { 0x199, "remove_target_knockback" }, { 0x19a, "remove_attacker_knockback" }, + { 0x19b, "set_global_script_type" }, + { 0x19c, "available_global_script_types" }, { 0x19d, "set_sfall_global" }, { 0x19e, "get_sfall_global_int" }, { 0x19f, "get_sfall_global_float" }, + { 0x1a0, "set_pickpocket_max" }, + { 0x1a1, "set_hit_chance_max" }, + { 0x1a2, "set_skill_max" }, + { 0x1a3, "eax_available" }, + { 0x1a4, "set_eax_environment" }, { 0x1a5, "inc_npc_level" }, + { 0x1a6, "get_viewport_x" }, + { 0x1a7, "get_viewport_y" }, + { 0x1a8, "set_viewport_x" }, + { 0x1a9, "set_viewport_y" }, { 0x1aa, "set_xp_mod" }, + { 0x1ab, "set_perk_level_mod" }, { 0x1ac, "get_ini_setting" }, + { 0x1ad, "get_shader_version" }, + { 0x1ae, "set_shader_mode" }, + { 0x1af, "get_game_mode" }, + { 0x1b0, "force_graphics_refresh" }, + { 0x1b1, "get_shader_texture" }, + { 0x1b2, "set_shader_texture" }, + { 0x1b3, "get_uptime" }, + { 0x1b4, "set_stat_max" }, + { 0x1b5, "set_stat_min" }, + { 0x1b6, "set_car_current_town" }, + { 0x1b7, "set_pc_stat_max" }, + { 0x1b8, "set_pc_stat_min" }, + { 0x1b9, "set_npc_stat_max" }, + { 0x1ba, "set_npc_stat_min" }, { 0x1bb, "set_fake_perk" }, { 0x1bc, "set_fake_trait" }, { 0x1bd, "set_selectable_perk" }, + { 0x1be, "set_perkbox_title" }, + { 0x1bf, "hide_real_perks" }, + { 0x1c0, "show_real_perks" }, { 0x1c1, "has_fake_perk" }, { 0x1c2, "has_fake_trait" }, + { 0x1c3, "perk_add_mode" }, + { 0x1c4, "clear_selectable_perks" }, { 0x1c5, "set_critter_hit_chance_mod" }, + { 0x1c6, "set_base_hit_chance_mod" }, { 0x1c7, "set_critter_skill_mod" }, + { 0x1c8, "set_base_skill_mod" }, { 0x1c9, "set_critter_pickpocket_mod" }, + { 0x1ca, "set_base_pickpocket_mod" }, + { 0x1cb, "set_pyromaniac_mod" }, + { 0x1cc, "apply_heaveho_fix" }, + { 0x1cd, "set_swiftlearner_mod" }, + { 0x1ce, "set_hp_per_level_mod" }, + { 0x1cf, "write_byte" }, + { 0x1d0, "write_short" }, + { 0x1d1, "write_int" }, + { 0x1d2, "call_offset_v0" }, + { 0x1d3, "call_offset_v1" }, + { 0x1d4, "call_offset_v2" }, + { 0x1d5, "call_offset_v3" }, + { 0x1d6, "call_offset_v4" }, + { 0x1d7, "call_offset_r0" }, + { 0x1d8, "call_offset_r1" }, + { 0x1d9, "call_offset_r2" }, + { 0x1da, "call_offset_r3" }, + { 0x1db, "call_offset_r4" }, { 0x1dc, "show_iface_tag" }, { 0x1dd, "hide_iface_tag" }, { 0x1de, "is_iface_tag_active" }, + { 0x1df, "get_bodypart_hit_modifier" }, + { 0x1e0, "set_bodypart_hit_modifier" }, { 0x1e1, "set_critical_table" }, { 0x1e2, "get_critical_table" }, { 0x1e3, "reset_critical_table" }, { 0x1e4, "get_sfall_arg" }, { 0x1e5, "set_sfall_return" }, + { 0x1e6, "set_unspent_ap_bonus" }, + { 0x1e7, "get_unspent_ap_bonus" }, + { 0x1e8, "set_unspent_ap_perk_bonus" }, + { 0x1e9, "get_unspent_ap_perk_bonus" }, + { 0x1ea, "init_hook" }, { 0x1eb, "get_ini_string" }, { 0x1ec, "sqrt" }, { 0x1ed, "abs" }, @@ -175,13 +276,14 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x1f3, "remove_script" }, { 0x1f4, "set_script" }, { 0x1f5, "get_script" }, + { 0x1f6, "nb_create_char" }, { 0x1f7, "fs_create" }, { 0x1f8, "fs_copy" }, { 0x1f9, "fs_find" }, { 0x1fa, "fs_write_byte" }, { 0x1fb, "fs_write_short" }, { 0x1fc, "fs_write_int" }, - { 0x1fd, "fs_write_int" }, + { 0x1fd, "fs_write_float" }, { 0x1fe, "fs_write_string" }, { 0x1ff, "fs_delete" }, { 0x200, "fs_size" }, @@ -190,6 +292,7 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x203, "fs_resize" }, { 0x204, "get_proto_data" }, { 0x205, "set_proto_data" }, + { 0x206, "set_self" }, { 0x207, "register_hook" }, { 0x208, "fs_write_bstring" }, { 0x209, "fs_read_byte" }, @@ -199,17 +302,35 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x20d, "list_begin" }, { 0x20e, "list_next" }, { 0x20f, "list_end" }, + { 0x210, "sfall_ver_major" }, + { 0x211, "sfall_ver_minor" }, + { 0x212, "sfall_ver_build" }, + { 0x213, "hero_select_win" }, + { 0x214, "set_hero_race" }, + { 0x215, "set_hero_style" }, { 0x216, "set_critter_burst_disable" }, { 0x217, "get_weapon_ammo_pid" }, { 0x218, "set_weapon_ammo_pid" }, { 0x219, "get_weapon_ammo_count" }, { 0x21a, "set_weapon_ammo_count" }, + { 0x21b, "write_string" }, + { 0x21c, "get_mouse_x" }, + { 0x21d, "get_mouse_y" }, { 0x21e, "get_mouse_buttons" }, + { 0x21f, "get_window_under_mouse" }, + { 0x220, "get_screen_width" }, + { 0x221, "get_screen_height" }, + { 0x222, "stop_game" }, + { 0x223, "resume_game" }, { 0x224, "create_message_window" }, + { 0x225, "remove_trait" }, + { 0x226, "get_light_level" }, + { 0x227, "refresh_pc_art" }, { 0x228, "get_attack_type" }, { 0x229, "force_encounter_with_flags" }, { 0x22a, "set_map_time_multi" }, { 0x22b, "play_sfall_sound" }, + { 0x22c, "stop_sfall_sound" }, { 0x22d, "create_array" }, { 0x22e, "set_array" }, { 0x22f, "get_array" }, @@ -227,21 +348,31 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x23b, "modified_ini" }, { 0x23c, "get_sfall_args" }, { 0x23d, "set_sfall_arg" }, + { 0x23e, "force_aimed_shots" }, + { 0x23f, "disable_aimed_shots" }, + { 0x240, "mark_movie_played" }, { 0x241, "get_npc_level" }, { 0x242, "set_critter_skill_points" }, { 0x243, "get_critter_skill_points" }, + { 0x244, "set_available_skill_points" }, + { 0x245, "get_available_skill_points" }, + { 0x246, "mod_skill_points_per_level" }, + { 0x247, "set_perk_freq" }, + { 0x248, "get_last_target" }, + { 0x249, "get_last_attacker" }, + { 0x24a, "block_combat" }, + { 0x24b, "tile_under_cursor" }, + { 0x24c, "gdialog_get_barter_mod" }, + { 0x24d, "set_inven_ap_cost" }, { 0x24e, "substr" }, { 0x24f, "strlen" }, { 0x250, "sprintf" }, { 0x251, "charcode" }, - // 0x25 { 0x253, "typeof" }, { 0x254, "save_array" }, { 0x255, "load_array" }, { 0x256, "array_key" }, { 0x257, "arrayexpr" }, - // 0x25 - // 0x25 { 0x25a, "reg_anim_destroy" }, { 0x25b, "reg_anim_animate_and_hide" }, { 0x25c, "reg_anim_combat_check" }, @@ -256,9 +387,6 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x265, "exponent" }, { 0x266, "ceil" }, { 0x267, "round" }, - // 0x26 - // 0x26 - // 0x26 { 0x26b, "message_str_game" }, { 0x26c, "sneak_success" }, { 0x26d, "tile_light" }, @@ -270,7 +398,6 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x273, "create_spatial" }, { 0x274, "art_exists" }, { 0x275, "obj_is_carrying_obj" }, - // universal opcodes { 0x276, "sfall_func0" }, { 0x277, "sfall_func1" }, { 0x278, "sfall_func2" }, @@ -278,13 +405,11 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = { { 0x27a, "sfall_func4" }, { 0x27b, "sfall_func5" }, { 0x27c, "sfall_func6" }, - { 0x27d, "register_hook_proc_spec" }, { 0x27e, "reg_anim_callback" }, { 0x27f, "div" }, - { 0x280, "sfall_func7" }, - { 0x281, "sfall_func8" } + { 0x281, "sfall_func8" }, }; #endif diff --git a/src/sfall_opcodes.cc b/src/sfall_opcodes.cc index 3fe16f44..939e6fa3 100644 --- a/src/sfall_opcodes.cc +++ b/src/sfall_opcodes.cc @@ -1942,7 +1942,7 @@ void sfallOpcodesInit() // 0x8183 - void set_perk_per(int perkID, int value) // 0x8184 - void set_perk_end(int perkID, int value) // 0x8185 - void set_perk_chr(int perkID, int value) - // 0x8196 - void set_perk_int(int perkID, int value) + // 0x8186 - void set_perk_int(int perkID, int value) // 0x8187 - void set_perk_agl(int perkID, int value) // 0x8188 - void set_perk_lck(int perkID, int value) // 0x8189 - void set_perk_name(int perkID, string value) From 41d908274fdceb04a0517d6aa67573ca8fd4f16c Mon Sep 17 00:00:00 2001 From: jirik2077 Date: Thu, 9 Jul 2026 20:05:06 +0200 Subject: [PATCH 2/4] Handle unimplemented fs_copy/fs_create/fs_find more gracefully in scripts (#530) * handle unimplemented fs_copy/fs_create/fs_find more gracefully from the script point of view * chore: auto-format with clang-format * moved to sfall_opcodes --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/sfall_opcodes.cc | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/sfall_opcodes.cc b/src/sfall_opcodes.cc index 939e6fa3..a8edb8df 100644 --- a/src/sfall_opcodes.cc +++ b/src/sfall_opcodes.cc @@ -1809,6 +1809,29 @@ static void op_set_sfall_return(Program* program) hookCall->addReturnValueFromScript(value); } +static void op_fs_copy(Program* program) +{ + char* source = programStackPopString(program); + char* path = programStackPopString(program); + programPrintError("fs_copy: not implemented!"); + programStackPushInteger(program, -1); +} + +static void op_fs_find(Program* program) +{ + char* path = programStackPopString(program); + programPrintError("fs_find: not implemented!"); + programStackPushInteger(program, -1); +} + +static void op_fs_create(Program* program) +{ + int size = programStackPopInteger(program); + char* path = programStackPopString(program); + programPrintError("fs_create: not implemented!"); + programStackPushInteger(program, -1); +} + // Note: opcodes should pop arguments off the stack in reverse order void sfallOpcodesInit() { @@ -2142,8 +2165,11 @@ void sfallOpcodesInit() // 0x81f6 - int nb_create_char() // deprecated; do not implement // 0x81f7 - int fs_create(string path, int size) + interpreterRegisterOpcode(0x81f7, op_fs_create); // 0x81f8 - int fs_copy(string path, string source) + interpreterRegisterOpcode(0x81f8, op_fs_copy); // 0x81f9 - int fs_find(string path) + interpreterRegisterOpcode(0x81f9, op_fs_find); // 0x81fa - void fs_write_byte(int id, int data) // 0x81fb - void fs_write_short(int id, int data) // 0x81fc - void fs_write_int(int id, int data) From 9c19d212a63259c746855ca568ed0e9bfbee2635 Mon Sep 17 00:00:00 2001 From: Mike Klaas Date: Thu, 9 Jul 2026 17:43:28 -0700 Subject: [PATCH 3/4] Fix blue/green channel mixup in PCX reading (#500) --- src/datafile.cc | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/datafile.cc b/src/datafile.cc index c7c39fe4..a73c0cf1 100644 --- a/src/datafile.cc +++ b/src/datafile.cc @@ -49,10 +49,9 @@ void datafileRemapPixelsRgb8(uint8_t* data, uint8_t* palette, int width, int hei indexedPalette[0] = 0; for (int index = 1; index < INDEXED_PALETTE_MAX; index++) { - // TODO: Check. - int r = palette[index * 3 + 2] >> 3; + int r = palette[index * 3] >> 3; int g = palette[index * 3 + 1] >> 3; - int b = palette[index * 3] >> 3; + int b = palette[index * 3 + 2] >> 3; int colorTableIndex = (r << 10) | (g << 5) | b; indexedPalette[index] = _colorTable[colorTableIndex]; } @@ -72,10 +71,9 @@ void datafileRemapPixelsRgb6(uint8_t* data, uint8_t* palette, int width, int hei indexedPalette[0] = 0; for (int index = 1; index < INDEXED_PALETTE_MAX; index++) { - // TODO: Check. - int r = palette[index * 3 + 2] >> 1; + int r = palette[index * 3] >> 1; int g = palette[index * 3 + 1] >> 1; - int b = palette[index * 3] >> 1; + int b = palette[index * 3 + 2] >> 1; int colorTableIndex = (r << 10) | (g << 5) | b; indexedPalette[index] = _colorTable[colorTableIndex]; } From ef0085dccaf04f0c7befd801104e558a4bbd1349 Mon Sep 17 00:00:00 2001 From: Mike Klaas Date: Fri, 10 Jul 2026 12:37:03 -0700 Subject: [PATCH 4/4] Support Sfall extra animation codes (#535) Fixes #534 --- src/art.cc | 27 ++++++++++++++++++++++++--- src/art.h | 5 +++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/art.cc b/src/art.cc index 7bbf6c05..78d49256 100644 --- a/src/art.cc +++ b/src/art.cc @@ -47,6 +47,7 @@ static int artReadFrameData(unsigned char* data, File* stream, int count, int* p static int artReadHeader(Art* art, File* stream); static int artGetDataSize(const Art* art); static int paddingForSize(int size); +static char artGetCritterWeaponCode(int weaponType); // A frame is laid out like [ArtFrame header][pixel bytes][padding]. // These functions return a pointer to the pixel bytes, but must be given a pointer to a frame header, @@ -583,7 +584,7 @@ int _art_get_code(int animation, int weaponType, char* weaponCodePtr, char* anim return -1; } - *weaponCodePtr = 'd' + (weaponType - 1); + *weaponCodePtr = artGetCritterWeaponCode(weaponType); return 0; } else if (animation == ANIM_PRONE_TO_STANDING) { *animationCodePtr = 'h'; @@ -625,7 +626,7 @@ int _art_get_code(int animation, int weaponType, char* weaponCodePtr, char* anim *weaponCodePtr = 'a'; *animationCodePtr = 'n'; } else { - *weaponCodePtr = 'd' + (weaponType - 1); + *weaponCodePtr = artGetCritterWeaponCode(weaponType); *animationCodePtr = 'e'; } return 0; @@ -633,7 +634,7 @@ int _art_get_code(int animation, int weaponType, char* weaponCodePtr, char* anim *animationCodePtr = 'a' + animation; if (animation <= ANIM_WALK && weaponType > 0) { - *weaponCodePtr = 'd' + (weaponType - 1); + *weaponCodePtr = artGetCritterWeaponCode(weaponType); return 0; } *weaponCodePtr = 'a'; @@ -641,6 +642,24 @@ int _art_get_code(int animation, int weaponType, char* weaponCodePtr, char* anim return 0; } +static char artGetCritterWeaponCode(int weaponType) +{ + switch (weaponType) { + case WEAPON_ANIMATION_SFALL_S: + return 's'; + case WEAPON_ANIMATION_SFALL_O: + return 'o'; + case WEAPON_ANIMATION_SFALL_P: + return 'p'; + case WEAPON_ANIMATION_SFALL_Q: + return 'q'; + case WEAPON_ANIMATION_SFALL_T: + return 't'; + default: + return 'd' + (weaponType - 1); + } +} + // 0x419428 char* artBuildFilePath(int fid) { @@ -957,9 +976,11 @@ int artAliasFid(int fid) int anim = FID_ANIM_TYPE(fid); if (type == OBJ_TYPE_CRITTER) { if (anim == ANIM_ELECTRIFY + || anim == ANIM_CHARRED_BODY || anim == ANIM_BURNED_TO_NOTHING || anim == ANIM_ELECTRIFIED_TO_NOTHING || anim == ANIM_ELECTRIFY_SF + || anim == ANIM_CHARRED_BODY_SF || anim == ANIM_BURNED_TO_NOTHING_SF || anim == ANIM_ELECTRIFIED_TO_NOTHING_SF || anim == ANIM_FIRE_DANCE diff --git a/src/art.h b/src/art.h index b9d7f740..56020255 100644 --- a/src/art.h +++ b/src/art.h @@ -100,6 +100,11 @@ typedef enum WeaponAnimation { WEAPON_ANIMATION_LASER_RIFLE, // k WEAPON_ANIMATION_MINIGUN, // l WEAPON_ANIMATION_LAUNCHER, // m + WEAPON_ANIMATION_SFALL_S, // s + WEAPON_ANIMATION_SFALL_O, // o + WEAPON_ANIMATION_SFALL_P, // p + WEAPON_ANIMATION_SFALL_Q, // q + WEAPON_ANIMATION_SFALL_T, // t WEAPON_ANIMATION_COUNT, } WeaponAnimation;