Implement all remaining sfall string functions (#184)

Implement string_to_case, and add comments for missing metarules/opcodes
    string_find
    string_compare
    charcode

This should complete all string functions listed here that aren't deprecated.


* chore: auto-format with clang-format

* PR feedback

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Mike Klaas
2025-06-02 10:12:25 +03:00
committed by GitHub
co-authored by github-actions[bot]
parent 04e93cabd7
commit 41dd2ccd81
3 changed files with 289 additions and 1 deletions
+14
View File
@@ -64,5 +64,19 @@ procedure start begin
REPEAT_1024,
REPEAT_1024)), 5119);
call assertEquals("string_tolower", string_tolower("miXeD CaSe"), "mixed case");
call assertEquals("string_toupper", string_toupper("miXeD CaSe"), "MIXED CASE");
call assertEquals("string_find ok", string_find("Hello World", "World"), 6);
call assertEquals("string_find missing", string_find("Hello World", "Zebra"), -1);
call assertEquals("string_find_from", string_find_from("Hello World", "o", 6), 7);
call assertEquals("string_compare ok", string_compare("Hello World", "HeLlO WoRld"), 1);
call assertEquals("string_compare neq", string_compare("Hello World", "HeLlO WoRld!"), 0);
call assertEquals("string_compare_locale", string_compare_locale("Hello World", "HeLlO WoRld", 866), 1); // TODO: would be nice to compare cyrillic
call assertEquals("charcode", charcode("A"), 65);
call assertEquals("charcode empty", charcode(""), 0);
call report_test_results("sprintf");
end
+221 -1
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <memory>
#include <string.h>
#include <string>
#include "combat.h"
#include "debug.h"
@@ -12,6 +13,7 @@
#include "interface.h"
#include "inventory.h"
#include "object.h"
#include "platform_compat.h"
#include "sfall_ini.h"
#include "text_font.h"
#include "tile.h"
@@ -48,28 +50,115 @@ static void mf_set_ini_setting(Program* program, int args);
static void mf_set_outline(Program* program, int args);
static void mf_show_window(Program* program, int args);
static void mf_tile_refresh_display(Program* program, int args);
static void mf_string_compare(Program* program, int args);
static void mf_string_find(Program* program, int args);
static void mf_string_to_case(Program* program, int args);
static void mf_string_format(Program* program, int args);
// ref. https://github.com/sfall-team/sfall/blob/42556141127895c27476cd5242a73739cbb0fade/sfall/Modules/Scripting/Handlers/Metarule.cpp#L72
constexpr MetaruleInfo kMetarules[] = {
// {"add_extra_msg_file", mf_add_extra_msg_file, 1, 2, -1, {ARG_STRING, ARG_INT}},
// {"add_iface_tag", mf_add_iface_tag, 0, 0},
// {"add_g_timer_event", mf_add_g_timer_event, 2, 2, -1, {ARG_INT, ARG_INT}},
// {"add_trait", mf_add_trait, 1, 1, -1, {ARG_INT}},
// {"art_cache_clear", mf_art_cache_flush, 0, 0},
// {"art_frame_data", mf_art_frame_data, 1, 3, 0, {ARG_INTSTR, ARG_INT, ARG_INT}},
// {"attack_is_aimed", mf_attack_is_aimed, 0, 0},
{ "car_gas_amount", mf_car_gas_amount, 0, 0 },
{ "combat_data", mf_combat_data, 0, 0 },
// {"create_win", mf_create_win, 5, 6, -1, {ARG_STRING, ARG_INT, ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
{ "critter_inven_obj2", mf_critter_inven_obj2, 2, 2 },
// {"dialog_message", mf_dialog_message, 1, 1, -1, {ARG_STRING}},
{ "dialog_obj", mf_dialog_obj, 0, 0 },
// {"display_stats", mf_display_stats, 0, 0}, // refresh
// {"draw_image", mf_draw_image, 1, 5, -1, {ARG_INTSTR, ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
// {"draw_image_scaled", mf_draw_image_scaled, 1, 6, -1, {ARG_INTSTR, ARG_INT, ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
// {"exec_map_update_scripts", mf_exec_map_update_scripts, 0, 0},
// {"floor2", mf_floor2, 1, 1, 0, {ARG_NUMBER}},
// {"get_can_rest_on_map", mf_get_rest_on_map, 2, 2, -1, {ARG_INT, ARG_INT}},
// {"get_combat_free_move", mf_get_combat_free_move, 0, 0},
// {"get_current_inven_size", mf_get_current_inven_size, 1, 1, 0, {ARG_OBJECT}},
{ "get_cursor_mode", mf_get_cursor_mode, 0, 0 },
{ "get_flags", mf_get_flags, 1, 1 },
// {"get_ini_config", mf_get_ini_config, 2, 2, 0, {ARG_STRING, ARG_INT}},
// {"get_ini_section", mf_get_ini_section, 2, 2, -1, {ARG_STRING, ARG_STRING}},
// {"get_ini_sections", mf_get_ini_sections, 1, 1, -1, {ARG_STRING}},
// {"get_inven_ap_cost", mf_get_inven_ap_cost, 0, 0},
// {"get_map_enter_position", mf_get_map_enter_position, 0, 0},
// {"get_metarule_table", mf_get_metarule_table, 0, 0},
// {"get_object_ai_data", mf_get_object_ai_data, 2, 2, -1, {ARG_OBJECT, ARG_INT}},
{ "get_object_data", mf_get_object_data, 2, 2 },
// {"get_outline", mf_get_outline, 1, 1, 0, {ARG_OBJECT}},
// {"get_sfall_arg_at", mf_get_sfall_arg_at, 1, 1, 0, {ARG_INT}},
// {"get_stat_max", mf_get_stat_max, 1, 2, 0, {ARG_INT, ARG_INT}},
// {"get_stat_min", mf_get_stat_min, 1, 2, 0, {ARG_INT, ARG_INT}},
// {"get_string_pointer", mf_get_string_pointer, 1, 1, 0, {ARG_STRING}}, // note: deprecated; do not implement
// {"get_terrain_name", mf_get_terrain_name, 0, 2, -1, {ARG_INT, ARG_INT}},
{ "get_text_width", mf_get_text_width, 1, 1 },
// {"get_window_attribute", mf_get_window_attribute, 1, 2, -1, {ARG_INT, ARG_INT}},
// {"has_fake_perk_npc", mf_has_fake_perk_npc, 2, 2, 0, {ARG_OBJECT, ARG_STRING}},
// {"has_fake_trait_npc", mf_has_fake_trait_npc, 2, 2, 0, {ARG_OBJECT, ARG_STRING}},
// {"hide_window", mf_hide_window, 0, 1, -1, {ARG_STRING}},
// {"interface_art_draw", mf_interface_art_draw, 4, 6, -1, {ARG_INT, ARG_INTSTR, ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
// {"interface_overlay", mf_interface_overlay, 2, 6, -1, {ARG_INT, ARG_INT, ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
// {"interface_print", mf_interface_print, 5, 6, -1, {ARG_STRING, ARG_INT, ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
// {"intface_hide", mf_intface_hide, 0, 0},
// {"intface_is_hidden", mf_intface_is_hidden, 0, 0},
{ "intface_redraw", mf_intface_redraw, 0, 1 },
// {"intface_show", mf_intface_show, 0, 0},
// {"inventory_redraw", mf_inventory_redraw, 0, 1, -1, {ARG_INT}},
// {"item_make_explosive", mf_item_make_explosive, 3, 4, -1, {ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
// {"item_weight", mf_item_weight, 1, 1, 0, {ARG_OBJECT}},
// {"lock_is_jammed", mf_lock_is_jammed, 1, 1, 0, {ARG_OBJECT}},
{ "loot_obj", mf_loot_obj, 0, 0 },
// {"message_box", mf_message_box, 1, 4, -1, {ARG_STRING, ARG_INT, ARG_INT, ARG_INT}},
{ "metarule_exist", mf_metarule_exist, 1, 1 },
// {"npc_engine_level_up", mf_npc_engine_level_up, 1, 1},
// {"obj_is_openable", mf_obj_is_openable, 1, 1, 0, {ARG_OBJECT}},
// {"obj_under_cursor", mf_obj_under_cursor, 2, 2, 0, {ARG_INT, ARG_INT}},
// {"objects_in_radius", mf_objects_in_radius, 3, 4, 0, {ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
{ "outlined_object", mf_outlined_object, 0, 0 },
// {"real_dude_obj", mf_real_dude_obj, 0, 0},
// {"reg_anim_animate_and_move", mf_reg_anim_animate_and_move, 4, 4, -1, {ARG_OBJECT, ARG_INT, ARG_INT, ARG_INT}},
// {"remove_timer_event", mf_remove_timer_event, 0, 1, -1, {ARG_INT}},
// {"set_spray_settings", mf_set_spray_settings, 4, 4, -1, {ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
// {"set_can_rest_on_map", mf_set_rest_on_map, 3, 3, -1, {ARG_INT, ARG_INT, ARG_INT}},
// {"set_car_intface_art", mf_set_car_intface_art, 1, 1, -1, {ARG_INT}},
// {"set_combat_free_move", mf_set_combat_free_move, 1, 1, -1, {ARG_INT}},
{ "set_cursor_mode", mf_set_cursor_mode, 1, 1 },
// {"set_drugs_data", mf_set_drugs_data, 3, 3, -1, {ARG_INT, ARG_INT, ARG_INT}},
// {"set_dude_obj", mf_set_dude_obj, 1, 1, -1, {ARG_INT}},
// {"set_fake_perk_npc", mf_set_fake_perk_npc, 5, 5, -1, {ARG_OBJECT, ARG_STRING, ARG_INT, ARG_INT, ARG_STRING}},
// {"set_fake_trait_npc", mf_set_fake_trait_npc, 5, 5, -1, {ARG_OBJECT, ARG_STRING, ARG_INT, ARG_INT, ARG_STRING}},
{ "set_flags", mf_set_flags, 2, 2 },
// {"set_iface_tag_text", mf_set_iface_tag_text, 3, 3, -1, {ARG_INT, ARG_STRING, ARG_INT}},
{ "set_ini_setting", mf_set_ini_setting, 2, 2 },
// {"set_map_enter_position", mf_set_map_enter_position, 3, 3, -1, {ARG_INT, ARG_INT, ARG_INT}},
// {"set_object_data", mf_set_object_data, 3, 3, -1, {ARG_OBJECT, ARG_INT, ARG_INT}},
{ "set_outline", mf_set_outline, 2, 2 },
// {"set_quest_failure_value", mf_set_quest_failure_value, 2, 2, -1, {ARG_INT, ARG_INT}},
// {"set_rest_heal_time", mf_set_rest_heal_time, 1, 1, -1, {ARG_INT}},
// {"set_worldmap_heal_time", mf_set_worldmap_heal_time, 1, 1, -1, {ARG_INT}},
// {"set_rest_mode", mf_set_rest_mode, 1, 1, -1, {ARG_INT}},
// {"set_scr_name", mf_set_scr_name, 0, 1, -1, {ARG_STRING}},
// {"set_selectable_perk_npc", mf_set_selectable_perk_npc, 5, 5, -1, {ARG_OBJECT, ARG_STRING, ARG_INT, ARG_INT, ARG_STRING}},
// {"set_terrain_name", mf_set_terrain_name, 3, 3, -1, {ARG_INT, ARG_INT, ARG_STRING}},
// {"set_town_title", mf_set_town_title, 2, 2, -1, {ARG_INT, ARG_STRING}},
// {"set_unique_id", mf_set_unique_id, 1, 2, -1, {ARG_OBJECT, ARG_INT}},
// {"set_unjam_locks_time", mf_set_unjam_locks_time, 1, 1, -1, {ARG_INT}},
// {"set_window_flag", mf_set_window_flag, 3, 3, -1, {ARG_INTSTR, ARG_INT, ARG_INT}},
{ "show_window", mf_show_window, 0, 1 },
{ "tile_refresh_display", mf_tile_refresh_display, 0, 0 },
// {"signal_close_game", mf_signal_close_game, 0, 0},
// {"spatial_radius", mf_spatial_radius, 1, 1, 0, {ARG_OBJECT}},
{ "string_compare", mf_string_compare, 2, 3 }, // {ARG_STRING, ARG_STRING, ARG_INT}},
{ "string_find", mf_string_find, 2, 3 }, // {ARG_STRING, ARG_STRING, ARG_INT}},
{ "string_format", mf_string_format, 2, 8 },
{ "string_to_case", mf_string_to_case, 2, 2 }, // {ARG_STRING, ARG_INT}}
// {"tile_by_position", mf_tile_by_position, 2, 2, -1, {ARG_INT, ARG_INT}},
{ "tile_refresh_display", mf_tile_refresh_display, 0, 0 },
// {"unjam_lock", mf_unjam_lock, 1, 1, -1, {ARG_OBJECT}},
// {"unwield_slot", mf_unwield_slot, 2, 2, -1, {ARG_OBJECT, ARG_INT}},
// {"win_fill_color", mf_win_fill_color, 0, 5, -1, {ARG_INT, ARG_INT, ARG_INT, ARG_INT, ARG_INT}},
};
constexpr int kMetarulesMax = sizeof(kMetarules) / sizeof(kMetarules[0]);
@@ -259,6 +348,137 @@ void mf_tile_refresh_display(Program* program, int args)
programStackPushInteger(program, -1);
}
// compares strings case-insensitive with specifics for Fallout
// from sfall: https://github.com/sfall-team/sfall/blob/71ecec3d405bd5e945f157954618b169e60068fe/sfall/Modules/Scripting/Handlers/Utils.cpp#L34
static bool FalloutStringCompare(const char* str1, const char* str2, long codePage)
{
while (true) {
unsigned char c1 = *str1;
unsigned char c2 = *str2;
if (c1 == 0 && c2 == 0) return true; // end - strings are equal
if (c1 == 0 || c2 == 0) return false; // strings are not equal
str1++;
str2++;
if (c1 == c2) continue;
if (codePage == 866) {
// replace Russian 'x' with English (Fallout specific)
if (c1 == 229) c1 -= 229 - 'x';
if (c2 == 229) c2 -= 229 - 'x';
}
// 0 - 127 (standard ASCII)
// upper to lower case
if (c1 >= 'A' && c1 <= 'Z') c1 |= 32;
if (c2 >= 'A' && c2 <= 'Z') c2 |= 32;
if (c1 == c2) continue;
if (c1 < 128 || c2 < 128) return false;
// 128 - 255 (international/extended)
switch (codePage) {
case 866:
if (c1 != 149 && c2 != 149) { // code used for the 'bullet' character in Fallout font (the Russian letter 'X' uses Latin letter)
// upper to lower case
if (c1 >= 128 && c1 <= 159) {
c1 |= 32;
} else if (c1 >= 224 && c1 <= 239) {
c1 -= 48; // shift lower range
} else if (c1 == 240) {
c1++;
}
if (c2 >= 128 && c2 <= 159) {
c2 |= 32;
} else if (c2 >= 224 && c2 <= 239) {
c2 -= 48; // shift lower range
} else if (c2 == 240) {
c2++;
}
}
break;
case 1251:
// upper to lower case
if (c1 >= 0xC0 && c1 <= 0xDF) c1 |= 32;
if (c2 >= 0xC0 && c2 <= 0xDF) c2 |= 32;
if (c1 == 0xA8) c1 += 16;
if (c2 == 0xA8) c2 += 16;
break;
case 1250:
case 1252:
if (c1 != 0xD7 && c1 != 0xF7 && c2 != 0xD7 && c2 != 0xF7) {
if (c1 >= 0xC0 && c1 <= 0xDE) c1 |= 32;
if (c2 >= 0xC0 && c2 <= 0xDE) c2 |= 32;
}
break;
}
if (c1 != c2) return false; // strings are not equal
}
}
void mf_string_compare(Program* program, int args)
{
// compare str1 to str3 case insensitively
// if args == 3, use FalloutStringCompare
const char* str1 = programStackPopString(program);
const char* str2 = programStackPopString(program);
int codePage = 0;
if (args == 3) {
codePage = programStackPopInteger(program);
}
bool result = false;
if (args < 3) {
// default case-insensitive comparison
result = compat_stricmp(str1, str2) == 0;
} else {
// Fallout specific case-insensitive comparison
result = FalloutStringCompare(str1, str2, codePage);
}
if (result) {
programStackPushInteger(program, 1); // strings are equal
} else {
programStackPushInteger(program, 0); // strings are not equal
}
}
void mf_string_find(Program* program, int args)
{
const char* str = programStackPopString(program);
const char* substr = programStackPopString(program);
int startPos = 0;
if (args == 3) {
startPos = programStackPopInteger(program);
}
if (startPos < 0 || startPos >= strlen(str)) {
debugPrint("string_find: invalid start position %d", startPos);
programStackPushInteger(program, -1);
return;
}
const char* found = strstr(str + startPos, substr);
if (found) {
programStackPushInteger(program, found - str);
} else {
programStackPushInteger(program, -1);
}
}
void mf_string_to_case(Program* program, int args)
{
auto buf = programStackPopString(program);
std::string s(buf);
auto caseType = programStackPopInteger(program);
if (caseType == 1) {
std::transform(s.begin(), s.end(), s.begin(), ::toupper);
} else if (caseType == 0) {
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
} else {
debugPrint("string_to_case: invalid case type %d", caseType);
}
programStackPushString(program, s.c_str());
}
void mf_string_format(Program* program, int args)
{
sprintf_lite(program, args, "string_format");
+54
View File
@@ -1098,9 +1098,21 @@ static void op_sprintf(Program* program)
sprintf_lite(program, 2, "op_sprintf");
}
static void op_charcode(Program* program)
{
const char* str = programStackPopString(program);
if (str != nullptr && str[0] != '\0') {
programStackPushInteger(program, static_cast<int>(str[0]));
} else {
programStackPushInteger(program, 0);
}
}
void sfallOpcodesInit()
{
// ref. https://github.com/sfall-team/sfall/blob/71ecec3d405bd5e945f157954618b169e60068fe/artifacts/scripting/sfall%20opcode%20list.txt#L145
interpreterRegisterOpcode(0x8156, op_read_byte);
// missing: read_short, read_int, read_string
interpreterRegisterOpcode(0x815A, op_set_pc_base_stat);
interpreterRegisterOpcode(0x815B, op_set_pc_bonus_stat);
interpreterRegisterOpcode(0x815C, op_get_pc_base_stat);
@@ -1112,46 +1124,74 @@ void sfallOpcodesInit()
interpreterRegisterOpcode(0x8162, op_tap_key);
interpreterRegisterOpcode(0x8163, op_get_year);
interpreterRegisterOpcode(0x8164, op_game_loaded);
// missing: graphics_funcs_available, load_shader, free_shader, activate_shader, deactivate_shader
interpreterRegisterOpcode(0x816A, op_set_global_script_repeat);
// missing: input_funcs_available
interpreterRegisterOpcode(0x816C, op_key_pressed);
// missing: set_shader_int, set_shader_float, set_shader_vector
interpreterRegisterOpcode(0x8170, op_in_world_map);
interpreterRegisterOpcode(0x8171, op_force_encounter);
interpreterRegisterOpcode(0x8172, op_set_world_map_pos);
// missing: many set_perk, kill_counter, critter_ap, dm/df_model functions
interpreterRegisterOpcode(0x8193, op_get_current_hand);
// missing: toggle_active_hand, 6 knockback functions
interpreterRegisterOpcode(0x819B, op_set_global_script_type);
interpreterRegisterOpcode(0x819D, op_set_sfall_global);
interpreterRegisterOpcode(0x819E, op_get_sfall_global_int);
// missing: get_sfall_global_float, skill_max, eax, npc_level, viewport, mod
interpreterRegisterOpcode(0x81AC, op_get_ini_setting);
// missing: get_shader_version, get_shader_mode
interpreterRegisterOpcode(0x81AF, op_get_game_mode);
interpreterRegisterOpcode(0x81B3, op_get_uptime);
// missing: set_stat_max, set_stat_min
interpreterRegisterOpcode(0x81B6, op_set_car_current_town);
// missing: set_pc/npc_stat_max/min, many fake_perk, fake_trait functions
// missing: set_critter/base_hit_chance_mod
// missing: write_byte/short/int
// missing: call_offset_v/r0-4
// missing: show/hide_iface_tag, is_face_tag_active
interpreterRegisterOpcode(0x81DF, op_get_bodypart_hit_modifier);
interpreterRegisterOpcode(0x81E0, op_set_bodypart_hit_modifier);
// missing: set/get/reset_critical_table
// missing: get_sfall_arg, set_sfall_return
// missing: set/get_unspend_ap_bonus/ebonus
// missing: init_hook
interpreterRegisterOpcode(0x81EB, op_get_ini_string);
interpreterRegisterOpcode(0x81EC, op_sqrt);
interpreterRegisterOpcode(0x81ED, op_abs);
// missing: sin, cos, tan, atan
// missing: set_palette
// missing: remove_script, set_script
interpreterRegisterOpcode(0x81F5, op_get_script);
// missing: nb_create_char, fs_*
interpreterRegisterOpcode(0x8204, op_get_proto_data);
interpreterRegisterOpcode(0x8205, op_set_proto_data);
interpreterRegisterOpcode(0x8206, op_set_self);
// missing: register_hook, more fs_*
interpreterRegisterOpcode(0x820D, op_list_begin);
interpreterRegisterOpcode(0x820E, op_list_next);
interpreterRegisterOpcode(0x820F, op_list_end);
interpreterRegisterOpcode(0x8210, op_get_version_major);
interpreterRegisterOpcode(0x8211, op_get_version_minor);
interpreterRegisterOpcode(0x8212, op_get_version_patch);
// missing: hero_select_win, set_hero_race, set_hero_style, set_critter_burst_disable
interpreterRegisterOpcode(0x8217, op_get_weapon_ammo_pid);
interpreterRegisterOpcode(0x8218, op_set_weapon_ammo_pid);
interpreterRegisterOpcode(0x8219, op_get_weapon_ammo_count);
interpreterRegisterOpcode(0x821A, op_set_weapon_ammo_count);
// missing: write_string
interpreterRegisterOpcode(0x821C, op_get_mouse_x);
interpreterRegisterOpcode(0x821D, op_get_mouse_y);
interpreterRegisterOpcode(0x821E, op_get_mouse_buttons);
// missing: get_window_under_mouse
interpreterRegisterOpcode(0x8220, op_get_screen_width);
interpreterRegisterOpcode(0x8221, op_get_screen_height);
// missing: stop_game, resume_game
interpreterRegisterOpcode(0x8224, op_create_message_window);
// missing: remove_trait, get_light_level, refresh_pc_art
interpreterRegisterOpcode(0x8228, op_get_attack_type);
interpreterRegisterOpcode(0x8229, op_force_encounter_with_flags);
// missing: set_map_time_multi, play_sfall_sound, stop_sfall_sound
interpreterRegisterOpcode(0x822D, op_create_array);
interpreterRegisterOpcode(0x822E, op_set_array);
interpreterRegisterOpcode(0x822F, op_get_array);
@@ -1165,21 +1205,34 @@ void sfallOpcodesInit()
interpreterRegisterOpcode(0x8237, op_parse_int);
interpreterRegisterOpcode(0x8238, op_atof);
interpreterRegisterOpcode(0x8239, op_scan_array);
// missing: modified_ini, get_sfall_args, set_sfall_arg, bunch of aimed shot, skillpoint, combat funcs
interpreterRegisterOpcode(0x824B, op_tile_under_cursor);
// missing: get_barter_mod, set_inven_ap_cost
interpreterRegisterOpcode(0x824E, op_substr);
interpreterRegisterOpcode(0x824F, op_get_string_length);
interpreterRegisterOpcode(0x8250, op_sprintf);
interpreterRegisterOpcode(0x8251, op_charcode);
interpreterRegisterOpcode(0x8253, op_type_of);
// missing: save_array, load_array
interpreterRegisterOpcode(0x8256, op_get_array_key);
interpreterRegisterOpcode(0x8257, op_stack_array);
// missing: <reserved>x2, reg_anim_*
interpreterRegisterOpcode(0x8261, op_explosions_metarule);
// missing: register_hook_proc
// missing: log
interpreterRegisterOpcode(0x8263, op_power);
// missing: ceil
interpreterRegisterOpcode(0x8267, op_round);
// 3 reserved opcodes
interpreterRegisterOpcode(0x826B, op_get_message);
// missing: sneak_success, tile_light
interpreterRegisterOpcode(0x826E, op_make_straight_path);
interpreterRegisterOpcode(0x826F, op_obj_blocking_at);
// missing: tile_get_objects
interpreterRegisterOpcode(0x8271, op_party_member_list);
// missing: path_find, create_spatial
interpreterRegisterOpcode(0x8274, op_art_exists);
// missing: is_carrying_obj
interpreterRegisterOpcode(0x8276, op_sfall_func0);
interpreterRegisterOpcode(0x8277, op_sfall_func1);
interpreterRegisterOpcode(0x8278, op_sfall_func2);
@@ -1187,6 +1240,7 @@ void sfallOpcodesInit()
interpreterRegisterOpcode(0x827A, op_sfall_func4);
interpreterRegisterOpcode(0x827B, op_sfall_func5);
interpreterRegisterOpcode(0x827C, op_sfall_func6);
// missing: register_hook_proc2, reg_anim_callback
interpreterRegisterOpcode(0x8280, op_sfall_func7);
interpreterRegisterOpcode(0x8281, op_sfall_func8);
interpreterRegisterOpcode(0x827F, op_div);