From 8ce4361365f579f734b501cb415c9525306f20f4 Mon Sep 17 00:00:00 2001 From: Mike Klaas Date: Sun, 28 Jun 2026 15:18:06 -0700 Subject: [PATCH] Windows opcodes (#505) * create_win / hide_window * get_window_attributes * get_window_under_mouse * set_window_flag * dialog_message * display_stats --- SFALL_COMPATIBILITY.md | 4 +- src/automap.cc | 9 ++ src/automap.h | 1 + src/character_editor.cc | 25 +++- src/character_editor.h | 2 + src/game_dialog.cc | 5 + src/game_dialog.h | 1 + src/inventory.cc | 94 ++++++++++---- src/inventory.h | 3 + src/options.cc | 21 +++- src/options.h | 1 + src/pipboy.cc | 7 +- src/pipboy.h | 1 + src/sfall_metarules.cc | 272 ++++++++++++++++++++++++++++++++++++++-- src/sfall_opcodes.cc | 7 ++ src/skilldex.cc | 7 +- src/skilldex.h | 1 + src/window.cc | 101 ++++++++++++--- src/window.h | 3 + src/window_manager.cc | 7 +- src/window_manager.h | 1 + src/worldmap.cc | 5 + src/worldmap.h | 1 + 23 files changed, 522 insertions(+), 57 deletions(-) diff --git a/SFALL_COMPATIBILITY.md b/SFALL_COMPATIBILITY.md index 0036202a..cdc6a17c 100644 --- a/SFALL_COMPATIBILITY.md +++ b/SFALL_COMPATIBILITY.md @@ -75,10 +75,10 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/) | Global scripts / Global script functions | set_global_script_repeat
set_global_script_type
available_global_script_types | ✅ except available_global_script_types | - | | Combat | attack_is_aimed
block_combat
force_aimed_shots
disable_aimed_shots
get_attack_type
get/set_bodypart_hit_modifier
combat_data
get/set/reset_critical_table
get_last_target
get_last_attacker
set_critter_burst_disable
get/set_critter_current_ap
set_spray_settings
get/set_combat_free_move | ✅ except block_combat, force_aimed_shots, disable_aimed_shots, get_last_target, get_last_attacker, set_spray_settings | - | | Car | set_car_current_town
car_gas_amount
set_car_intface_art | implemented: all except set_car_intface_art | - | -| Interface / Windows and images | art_frame_data
interface_art_draw
interface_print
draw_image
draw_image_scaled
get_window_under_mouse
create_win
get_window_attribute
message_box
set_window_flag
win_fill_color
interface_overlay
dialog_message
get_text_width
hide_window
show_window
create_message_window | implemented: only message_box, get_text_width, show_window, create_message_window | - | +| Interface / Windows and images | art_frame_data
interface_art_draw
interface_print
draw_image
draw_image_scaled
get_window_under_mouse
create_win
get_window_attribute
message_box
set_window_flag
win_fill_color
interface_overlay
dialog_message
get_text_width
hide_window
show_window
create_message_window | implemented: get_window_under_mouse, create_win, get_window_attribute, message_box, set_window_flag, dialog_message, get_text_width, hide_window, show_window, create_message_window | - | | Interface / Outline | outlined_object
get_outline
set_outline | ✅ | - | | Interface / Main interface | intface_is_hidden
intface_redraw
intface_hide
intface_show
set_quest_failure_value | implemented: only intface_redraw | `intface_redraw` only supports the zero-argument form; the optional-argument path is explicitly unimplemented. | -| Interface / Inventory | display_stats
inventory_redraw
critter_inven_obj2
get_current_inven_size
item_weight | implemented: critter_inven_obj2, item_weight | - | +| Interface / Inventory | display_stats
inventory_redraw
critter_inven_obj2
get_current_inven_size
item_weight | ✅ except get_current_inven_size | - | | Interface / Cursor | get/set_cursor_mode | ✅ | - | | Locks | lock_is_jammed
unjam_lock
set_unjam_locks_time | not implemented | - | | INI settings | get_ini_setting
get_ini_string
get_ini_section
get_ini_sections
get_ini_config
get_ini_config_db
set_ini_setting | ✅ | `modified_ini` is intentionally omitted as deprecated. | diff --git a/src/automap.cc b/src/automap.cc index eb6ccbfc..715ccec9 100644 --- a/src/automap.cc +++ b/src/automap.cc @@ -46,6 +46,8 @@ static void _decode_map_data(int elevation); static int automapCreate(); static int _copy_file_data(File* stream1, File* stream2, int length); +static int gAutomapWindow = -1; + typedef enum AutomapFrm { AUTOMAP_FRM_BACKGROUND, AUTOMAP_FRM_BUTTON_UP, @@ -326,6 +328,7 @@ void automapShow(bool isInGame, bool isUsingScanner) int automapWindowY = (screenGetHeight() - AUTOMAP_WINDOW_HEIGHT) / 2; // adding WINDOW_TRANSPARENT and WINDOW_DRAGGABLE_BY_BACKGROUND for testing temporarily int window = windowCreate(automapWindowX, automapWindowY, AUTOMAP_WINDOW_WIDTH, AUTOMAP_WINDOW_HEIGHT, color, WINDOW_MODAL | WINDOW_MOVE_ON_TOP | WINDOW_TRANSPARENT | WINDOW_DRAGGABLE_BY_BACKGROUND); + gAutomapWindow = window; int scannerBtn = buttonCreate(window, 111, @@ -493,10 +496,16 @@ void automapShow(bool isInGame, bool isUsingScanner) } windowDestroy(window); + gAutomapWindow = -1; fontSetCurrent(oldFont); touch_set_touchscreen_mode(false); } +int automapGetWindow() +{ + return windowGetWindow(gAutomapWindow) != nullptr ? gAutomapWindow : -1; +} + // Renders automap in Map window. // // 0x41BD1C draw_top_down_map diff --git a/src/automap.h b/src/automap.h index 6fe55064..8616ede3 100644 --- a/src/automap.h +++ b/src/automap.h @@ -55,6 +55,7 @@ void automapShow(bool isInGame, bool isUsingScanner); int automapRenderInPipboyWindow(int win, int map, int elevation); int automapSaveCurrent(); int automapGetHeader(AutomapHeader** automapHeaderPtr); +int automapGetWindow(); void automapSetDisplayMap(int map, bool available); diff --git a/src/character_editor.cc b/src/character_editor.cc index a58e8b6a..063564a5 100644 --- a/src/character_editor.cc +++ b/src/character_editor.cc @@ -663,7 +663,7 @@ static int gCharacterEditorPrimaryStatMinusBtns[7]; static unsigned char* gCharacterEditorWindowBuffer; // 0x57060C edit_win -static int gCharacterEditorWindow; +static int gCharacterEditorWindow = -1; // + stats buttons // @@ -5680,6 +5680,29 @@ void characterEditorReset() gCharacterEditorLastLevel = 1; } +int characterEditorGetWindow() +{ + return windowGetWindow(gCharacterEditorWindow) != nullptr ? gCharacterEditorWindow : -1; +} + +void characterEditorDisplayStats() +{ + if (windowGetWindow(gCharacterEditorWindow) == nullptr) { + return; + } + + critterUpdateDerivedStats(gDude); + critterAdjustHitPoints(gDude, 0); + + characterEditorDrawSkills(0); + characterEditorDrawPrimaryStat(RENDER_ALL_STATS, 0, 0); + characterEditorDrawPcStats(); + characterEditorDrawDerivedStats(); + characterEditorDrawFolders(); + characterEditorDrawCard(); + windowRefresh(gCharacterEditorWindow); +} + // level up if needed // // 0x43C228 UpdateLevel diff --git a/src/character_editor.h b/src/character_editor.h index af69e848..a1e28097 100644 --- a/src/character_editor.h +++ b/src/character_editor.h @@ -14,6 +14,8 @@ char* _strmfe(char* dest, const char* name, const char* ext); int characterEditorSave(File* stream); int characterEditorLoad(File* stream); void characterEditorReset(); +int characterEditorGetWindow(); +void characterEditorDisplayStats(); } // namespace fallout diff --git a/src/game_dialog.cc b/src/game_dialog.cc index c6864a5a..715f6056 100644 --- a/src/game_dialog.cc +++ b/src/game_dialog.cc @@ -3495,6 +3495,11 @@ bool gameDialogIsBarterWindowExpanded() return gBarterWindowExpanded; } +int gameDialogGetWindow() +{ + return windowGetWindow(gGameDialogWindow) != nullptr ? gGameDialogWindow : -1; +} + // 0x448660 gdialog_barter_cleanup_tables void gameDialogBarterCleanupTables() { diff --git a/src/game_dialog.h b/src/game_dialog.h index 10a616db..e35a289f 100644 --- a/src/game_dialog.h +++ b/src/game_dialog.h @@ -40,6 +40,7 @@ void gameDialogSetBarterModifier(int modifier); int gameDialogBarter(int modifier); void gameDialogEndBarter(); bool gameDialogIsBarterWindowExpanded(); +int gameDialogGetWindow(); } // namespace fallout diff --git a/src/inventory.cc b/src/inventory.cc index 68b2b2ff..648a2e35 100644 --- a/src/inventory.cc +++ b/src/inventory.cc @@ -352,6 +352,7 @@ static void inventoryNormalLayoutUpdate(); static bool inventoryBackgroundLoad(FrmImage& image, int col1FrmId, const char* col2Name, int columns); static int inventoryChooseColumns(FrmImage& image, int expandedWidth, int col1FrmId, const char* col2Name); static void inventoryCreateSlotButtons(int baseKeyCode, int scrollerX, int scrollerY, int columns); +static bool inventoryGetCurrentWindowType(InventoryWindowType* inventoryWindowTypePtr); static int inventoryGetWindowWidth(int inventoryWindowType); static int inventoryGetWindowHeight(int inventoryWindowType); static void inventoryNormalClampStackOffset(); @@ -594,7 +595,7 @@ static int gInventoryWindowDudeFid; static Inventory* _pud; // 0x59E964 i_wid -static int gInventoryWindow; +static int gInventoryWindow = -1; // item2 // 0x59E968 i_rhand @@ -695,6 +696,31 @@ static void inventoryCreateSlotButtons(int baseKeyCode, int scrollerX, int scrol } } +static bool inventoryGetCurrentWindowType(InventoryWindowType* inventoryWindowTypePtr) +{ + if (GameMode::isInGameMode(GameMode::kBarter)) { + *inventoryWindowTypePtr = INVENTORY_WINDOW_TYPE_TRADE; + return true; + } + + if (GameMode::isInGameMode(GameMode::kLoot)) { + *inventoryWindowTypePtr = INVENTORY_WINDOW_TYPE_LOOT; + return true; + } + + if (GameMode::isInGameMode(GameMode::kUseOn)) { + *inventoryWindowTypePtr = INVENTORY_WINDOW_TYPE_USE_ITEM_ON; + return true; + } + + if (GameMode::isInGameMode(GameMode::kInventory)) { + *inventoryWindowTypePtr = INVENTORY_WINDOW_TYPE_NORMAL; + return true; + } + + return false; +} + static int inventoryGetWindowWidth(int inventoryWindowType) { if (inventoryWindowType == INVENTORY_WINDOW_TYPE_NORMAL) return inventoryLayout.windowWidth; @@ -957,8 +983,6 @@ void inventoryOpen() } } - ScopedGameMode gm(GameMode::kInventory); - if (inventoryCommonInit() == -1) { return; } @@ -997,6 +1021,7 @@ void inventoryOpen() inventoryRenderSummary(); _display_inventory(_stack_offset[_curr_stack], -1, INVENTORY_WINDOW_TYPE_NORMAL); inventorySetCursor(INVENTORY_WINDOW_CURSOR_HAND); + ScopedGameMode gm(GameMode::kInventory); for (;;) { sharedFpsLimiter.mark(); @@ -2743,8 +2768,6 @@ static void _adjust_fid() // 0x4717E4 void inventoryOpenUseItemOn(Object* targetObj) { - ScopedGameMode gm(GameMode::kUseOn); - if (inventoryCommonInit() == -1) { return; } @@ -2752,6 +2775,7 @@ void inventoryOpenUseItemOn(Object* targetObj) bool isoWasEnabled = _setup_inventory(INVENTORY_WINDOW_TYPE_USE_ITEM_ON); _display_inventory(_stack_offset[_curr_stack], -1, INVENTORY_WINDOW_TYPE_USE_ITEM_ON); inventorySetCursor(INVENTORY_WINDOW_CURSOR_HAND); + ScopedGameMode gm(GameMode::kUseOn); for (;;) { sharedFpsLimiter.mark(); @@ -3088,7 +3112,7 @@ int objectGetCarriedQuantityByPid(Object* object, int pid) // Renders character's summary of SPECIAL stats, equipped armor bonuses, // and weapon's damage/range. // -// 0x471D5C +// 0x471D5C display_stats static void inventoryRenderSummary() { int summaryStats[7]; @@ -4350,8 +4374,6 @@ int inventoryOpenLooting(Object* looter, Object* target) return 0; } - ScopedGameMode gm(GameMode::kLoot); - if (FID_TYPE(target->fid) == OBJ_TYPE_CRITTER) { if (critterFlagCheck(target->pid, CRITTER_NO_STEAL)) { // You can't find anything to take from that. @@ -4417,6 +4439,7 @@ int inventoryOpenLooting(Object* looter, Object* target) } bool isoWasEnabled = _setup_inventory(INVENTORY_WINDOW_TYPE_LOOT); + ScopedGameMode gm(GameMode::kLoot); Object** critters = nullptr; int critterCount = 0; @@ -6338,6 +6361,46 @@ int inventorySetTimer(Object* item) return seconds; } +int inventoryGetWindow() +{ + return windowGetWindow(gInventoryWindow) != nullptr ? gInventoryWindow : -1; +} + +void inventoryDisplayStats() +{ + if (windowGetWindow(gInventoryWindow) == nullptr) { + return; + } + + inventoryRenderSummary(); + windowRefresh(gInventoryWindow); +} + +void inventoryRedraw(int redrawSide) +{ + if (windowGetWindow(gInventoryWindow) == nullptr) { + return; + } + + InventoryWindowType inventoryWindowType; + if (!inventoryGetCurrentWindowType(&inventoryWindowType)) { + return; + } + + if (redrawSide <= 0) { + _stack_offset[_curr_stack] = 0; + _display_inventory(0, -1, inventoryWindowType); + } + + if (redrawSide != 0 + && (inventoryWindowType == INVENTORY_WINDOW_TYPE_LOOT + || inventoryWindowType == INVENTORY_WINDOW_TYPE_TRADE)) { + _target_stack_offset[_target_curr_stack] = 0; + _display_target_inventory(0, -1, _target_pud, inventoryWindowType); + windowRefresh(gInventoryWindow); + } +} + Object* inventoryGetTargetObject() { return _target_stack[_target_curr_stack]; @@ -6350,10 +6413,8 @@ int inventoryUnwieldSlot(Object* critter, InvenSlot slot) } bool isDude = critter == gDude; - bool uiModeActive = GameMode::isInGameMode(GameMode::kInventory) - || GameMode::isInGameMode(GameMode::kUseOn) - || GameMode::isInGameMode(GameMode::kLoot) - || GameMode::isInGameMode(GameMode::kBarter); + InventoryWindowType inventoryWindowType; + bool uiModeActive = inventoryGetCurrentWindowType(&inventoryWindowType); bool update = false; if (slot != InvenSlot::Armor && !uiModeActive) { @@ -6443,15 +6504,6 @@ int inventoryUnwieldSlot(Object* critter, InvenSlot slot) return 0; } - InventoryWindowType inventoryWindowType = INVENTORY_WINDOW_TYPE_NORMAL; - if (GameMode::isInGameMode(GameMode::kBarter)) { - inventoryWindowType = INVENTORY_WINDOW_TYPE_TRADE; - } else if (GameMode::isInGameMode(GameMode::kLoot)) { - inventoryWindowType = INVENTORY_WINDOW_TYPE_LOOT; - } else if (GameMode::isInGameMode(GameMode::kUseOn)) { - inventoryWindowType = INVENTORY_WINDOW_TYPE_USE_ITEM_ON; - } - if (inventoryWindowType == INVENTORY_WINDOW_TYPE_NORMAL) { inventoryRenderSummary(); } diff --git a/src/inventory.h b/src/inventory.h index a67a8826..c68022d4 100644 --- a/src/inventory.h +++ b/src/inventory.h @@ -62,6 +62,9 @@ int inventoryOpenLooting(Object* looter, Object* target); int inventoryOpenStealing(Object* thief, Object* target); void barterProcessUI(int win, Object* barterer, Object* playerTable, Object* bartererTable, int barterMod); int inventorySetTimer(Object* item); +int inventoryGetWindow(); +void inventoryDisplayStats(); +void inventoryRedraw(int redrawSide); Object* inventoryGetTargetObject(); int inventoryUnwieldSlot(Object* critter, InvenSlot slot); diff --git a/src/options.cc b/src/options.cc index f44080d3..4e970a01 100644 --- a/src/options.cc +++ b/src/options.cc @@ -72,7 +72,9 @@ static unsigned char* _opbtns[OPTIONS_WINDOW_BUTTONS_COUNT]; static bool gOptionsWindowGameMouseObjectsWasVisible; // 0x663900 optnwin -static int gOptionsWindow; +static int gOptionsWindow = -1; + +static int gPauseWindow = -1; // 0x663908 winbuf static unsigned char* gOptionsWindowBuffer; @@ -303,6 +305,7 @@ static int optionsWindowInit() static int optionsWindowFree() { windowDestroy(gOptionsWindow); + gOptionsWindow = -1; fontSetCurrent(gOptionsWindowOldFont); messageListFree(&gPreferencesMessageList); @@ -389,6 +392,8 @@ int showPause(bool preserveWorldState) return -1; } + gPauseWindow = window; + unsigned char* windowBuffer = windowGetBuffer(window); memcpy(windowBuffer, frmImages[PAUSE_WINDOW_FRM_BACKGROUND].getData(), @@ -477,6 +482,7 @@ int showPause(bool preserveWorldState) } windowDestroy(window); + gPauseWindow = -1; messageListFree(&gPreferencesMessageList); if (!preserveWorldState) { @@ -529,4 +535,17 @@ int _init_options_menu() return 0; } +int optionsGetWindow() +{ + if (windowGetWindow(gOptionsWindow) != nullptr) { + return gOptionsWindow; + } + + if (windowGetWindow(gPauseWindow) != nullptr) { + return gPauseWindow; + } + + return -1; +} + } // namespace fallout diff --git a/src/options.h b/src/options.h index 5acb5828..e68548db 100644 --- a/src/options.h +++ b/src/options.h @@ -6,6 +6,7 @@ namespace fallout { int showOptions(); int showPause(bool preserveWorldState); int _init_options_menu(); +int optionsGetWindow(); } // namespace fallout diff --git a/src/pipboy.cc b/src/pipboy.cc index 6e258cf4..8ce195d5 100644 --- a/src/pipboy.cc +++ b/src/pipboy.cc @@ -354,7 +354,7 @@ int gPipboyPreviousMouseX; int gPipboyPreviousMouseY; // 0x6644C4 pip_win -int gPipboyWindow; +int gPipboyWindow = -1; // 0x6644F4 holodisk int _holodisk; @@ -1014,6 +1014,11 @@ int pipboyLoad(File* stream) return _save_pipboy(stream); } +int pipboyGetWindow() +{ + return windowGetWindow(gPipboyWindow) != nullptr ? gPipboyWindow : -1; +} + // 0x497BD8 static void pipboyWindowHandleStatus(int userInput) { diff --git a/src/pipboy.h b/src/pipboy.h index 4c6779d5..8ceeb33f 100644 --- a/src/pipboy.h +++ b/src/pipboy.h @@ -16,6 +16,7 @@ void pipboyInit(); void pipboyReset(); int pipboySave(File* stream); int pipboyLoad(File* stream); +int pipboyGetWindow(); extern MessageList gPipboyMessageList; int pipboyMessageListInit(); diff --git a/src/sfall_metarules.cc b/src/sfall_metarules.cc index 5d124ba0..2be88d6a 100644 --- a/src/sfall_metarules.cc +++ b/src/sfall_metarules.cc @@ -7,6 +7,8 @@ #include #include "art.h" +#include "automap.h" +#include "character_editor.h" #include "color.h" #include "combat.h" #include "config.h" // For Config, configInit, configFree @@ -24,6 +26,8 @@ #include "message.h" #include "object.h" #include "opcode_context.h" +#include "options.h" +#include "pipboy.h" #include "platform_compat.h" #include "proto_instance.h" #include "scripts.h" @@ -32,9 +36,11 @@ #include "sfall_ini.h" #include "sfall_opcodes.h" #include "sfall_script_hooks.h" +#include "skilldex.h" #include "text_font.h" #include "tile.h" #include "window.h" +#include "window_manager.h" #include "worldmap.h" #include @@ -44,8 +50,11 @@ namespace fallout { static void mf_attack_is_aimed(OpcodeContext& ctx); static void mf_car_gas_amount(OpcodeContext& ctx); static void mf_combat_data(OpcodeContext& ctx); +static void mf_create_win(OpcodeContext& ctx); static void mf_critter_inven_obj2(OpcodeContext& ctx); static void mf_dialog_obj(OpcodeContext& ctx); +static void mf_dialog_message(OpcodeContext& ctx); +static void mf_display_stats(OpcodeContext& ctx); static void mf_get_combat_free_move(OpcodeContext& ctx); static void mf_get_cursor_mode(OpcodeContext& ctx); static void mf_get_flags(OpcodeContext& ctx); @@ -54,7 +63,10 @@ static void mf_get_object_data(OpcodeContext& ctx); static void mf_get_outline(OpcodeContext& ctx); static void mf_get_sfall_arg_at(OpcodeContext& ctx); static void mf_get_text_width(OpcodeContext& ctx); +static void mf_get_window_attribute(OpcodeContext& ctx); +static void mf_hide_window(OpcodeContext& ctx); static void mf_intface_redraw(OpcodeContext& ctx); +static void mf_inventory_redraw(OpcodeContext& ctx); static void mf_item_weight(OpcodeContext& ctx); static void mf_loot_obj(OpcodeContext& ctx); static void mf_message_box(OpcodeContext& ctx); @@ -72,6 +84,7 @@ static void mf_set_cursor_mode(OpcodeContext& ctx); static void mf_set_flags(OpcodeContext& ctx); static void mf_set_iface_tag_text(OpcodeContext& ctx); static void mf_set_outline(OpcodeContext& ctx); +static void mf_set_window_flag(OpcodeContext& ctx); static void mf_set_unique_id(OpcodeContext& ctx); static void mf_show_window(OpcodeContext& ctx); static void mf_signal_close_game(OpcodeContext& ctx); @@ -93,15 +106,14 @@ const MetaruleInfo kMetarules[] = { // {"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}}, + { "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, 0, { ARG_OBJECT, ARG_INT } }, - // {"dialog_message", mf_dialog_message, 1, 1, -1, {ARG_STRING}}, + { "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 + { "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}, @@ -126,18 +138,17 @@ const MetaruleInfo kMetarules[] = { // {"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, 0, { ARG_STRING } }, - // {"get_window_attribute", mf_get_window_attribute, 1, 2, -1, {ARG_INT, ARG_INT}}, + { "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}}, + { "hide_window", mf_hide_window, 0, 1, -1, { ARG_STRING } }, // {"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}}, + { "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}}, @@ -177,7 +188,7 @@ const MetaruleInfo kMetarules[] = { // {"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}}, + { "set_window_flag", mf_set_window_flag, 3, 3, -1, { ARG_INTSTR, ARG_INT, ARG_INT } }, { "show_window", mf_show_window, 0, 1, -1, { ARG_STRING } }, { "signal_close_game", mf_signal_close_game, 0, 0 }, // {"spatial_radius", mf_spatial_radius, 1, 1, 0, {ARG_OBJECT}}, @@ -189,13 +200,102 @@ const MetaruleInfo kMetarules[] = { { "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}}, { "opcode_exists", mf_opcode_exists, 1, 1 }, }; const std::size_t kMetarulesCount = sizeof(kMetarules) / sizeof(kMetarules[0]); constexpr int kMetarulesMax = sizeof(kMetarules) / sizeof(kMetarules[0]); +enum class InterfaceWindowLookupResult { + Found, + Missing, + Invalid, +}; + +/* +// Valid window types for get_window_attribute +#define WINTYPE_INVENTORY (0) // any inventory window (player/loot/use/barter) +#define WINTYPE_DIALOG (1) +#define WINTYPE_PIPBOY (2) +#define WINTYPE_WORLDMAP (3) +#define WINTYPE_IFACEBAR (4) // the interface bar +#define WINTYPE_CHARACTER (5) +#define WINTYPE_SKILLDEX (6) +#define WINTYPE_ESCMENU (7) // escape menu +#define WINTYPE_AUTOMAP (8) +*/ +static InterfaceWindowLookupResult getInterfaceWindowByType(int winType, int& window) +{ + switch (winType) { + case 0: + window = inventoryGetWindow(); + break; + case 1: + window = gameDialogGetWindow(); + break; + case 2: + window = pipboyGetWindow(); + break; + case 3: + window = worldmapGetWindow(); + break; + case 4: + window = windowGetWindow(gInterfaceBarWindow) != nullptr ? gInterfaceBarWindow : -1; + break; + case 5: + window = characterEditorGetWindow(); + break; + case 6: + window = skilldexGetWindow(); + break; + case 7: + window = optionsGetWindow(); + break; + case 8: + window = automapGetWindow(); + break; + default: + window = -1; + return InterfaceWindowLookupResult::Invalid; + } + + return window != -1 ? InterfaceWindowLookupResult::Found : InterfaceWindowLookupResult::Missing; +} + +static int getCurrentInterfaceWindow() +{ + int window = -1; + if (GameMode::isInGameMode(GameMode::kInventory) + || GameMode::isInGameMode(GameMode::kUseOn) + || GameMode::isInGameMode(GameMode::kLoot) + || GameMode::isInGameMode(GameMode::kBarter)) { + window = inventoryGetWindow(); + } else if (GameMode::isInGameMode(GameMode::kDialog)) { + window = gameDialogGetWindow(); + } else if (GameMode::isInGameMode(GameMode::kPipboy)) { + window = pipboyGetWindow(); + } else if (GameMode::isInGameMode(GameMode::kWorldmap)) { + window = worldmapGetWindow(); + } else if (GameMode::isInGameMode(GameMode::kEditor)) { + window = characterEditorGetWindow(); + } else if (GameMode::isInGameMode(GameMode::kSkilldex)) { + window = skilldexGetWindow(); + } else if (GameMode::isInGameMode(GameMode::kOptions)) { + window = optionsGetWindow(); + } else if (GameMode::isInGameMode(GameMode::kAutomap)) { + window = automapGetWindow(); + } else if (windowGetWindow(gInterfaceBarWindow) != nullptr) { + window = gInterfaceBarWindow; + } + + return window; +} + +static bool applyWindowFlag(int windowId, int bitFlag, bool enabled) +{ + return scriptWindowSetFlag(windowId, bitFlag, enabled); +} + void mf_art_cache_flush(OpcodeContext& ctx) { artCacheFlush(); @@ -236,6 +336,35 @@ void mf_combat_data(OpcodeContext& ctx) } } +void mf_create_win(OpcodeContext& ctx) +{ + int flags = ctx.numArgs() > 5 + ? ctx.arg(5).asInt() + : WINDOW_MOVE_ON_TOP; + + int color = (flags & WINDOW_TRANSPARENT) != 0 ? 0 : 256; + if (scriptWindowCreate(ctx.stringArg(0), + ctx.arg(1).asInt(), + ctx.arg(2).asInt(), + ctx.arg(3).asInt(), + ctx.arg(4).asInt(), + color, + flags) + == -1) { + ctx.printError("%s() - couldn't create window.", ctx.name()); + ctx.setReturn(-1); + } +} + +void mf_display_stats(OpcodeContext& ctx) +{ + if (GameMode::isInGameMode(GameMode::kInventory)) { + inventoryDisplayStats(); + } else if (GameMode::isInGameMode(GameMode::kEditor)) { + characterEditorDisplayStats(); + } +} + void mf_critter_inven_obj2(OpcodeContext& ctx) { Object* obj = ctx.arg(0).asObject(); @@ -268,6 +397,14 @@ void mf_dialog_obj(OpcodeContext& ctx) } } +void mf_dialog_message(OpcodeContext& ctx) +{ + if (GameMode::isInGameMode(GameMode::kDialog) + && !GameMode::isInGameMode(GameMode::kDialogReview)) { + gameDialogRenderSupplementaryMessage(ctx.stringArg(0)); + } +} + void mf_get_combat_free_move(OpcodeContext& ctx) { ctx.setReturn(_combat_free_move); @@ -331,6 +468,67 @@ void mf_get_text_width(OpcodeContext& ctx) ctx.setReturn(fontGetStringWidth(ctx.stringArg(0))); } +void mf_get_window_attribute(OpcodeContext& ctx) +{ + int attrType = ctx.numArgs() > 1 ? ctx.arg(1).asInt() : 0; + int window = -1; + InterfaceWindowLookupResult lookup = getInterfaceWindowByType(ctx.arg(0).asInt(), window); + if (lookup == InterfaceWindowLookupResult::Missing) { + if (attrType != 0) { + ctx.printError("%s() - failed to get the interface window.", ctx.name()); + ctx.setReturn(-1); + } else { + ctx.setReturn(0); + } + return; + } + + if (lookup == InterfaceWindowLookupResult::Invalid) { + ctx.printError("%s() - invalid window type number.", ctx.name()); + ctx.setReturn(-1); + return; + } + + Rect rect; + if (windowGetRect(window, &rect) == -1) { + ctx.setReturn(-1); + return; + } + + switch (attrType) { + case -1: { + ArrayId arrayId = CreateTempArray(-1, 0); + SetArray(arrayId, programMakeString(ctx.program(), "left"), ProgramValue(rect.left), false, ctx.program()); + SetArray(arrayId, programMakeString(ctx.program(), "top"), ProgramValue(rect.top), false, ctx.program()); + SetArray(arrayId, programMakeString(ctx.program(), "right"), ProgramValue(rect.right), false, ctx.program()); + SetArray(arrayId, programMakeString(ctx.program(), "bottom"), ProgramValue(rect.bottom), false, ctx.program()); + ctx.setReturn(ProgramValue(arrayId)); + break; + } + case 0: // basically an existence check + ctx.setReturn(1); + break; + case 1: + ctx.setReturn(rect.left); + break; + case 2: + ctx.setReturn(rect.top); + break; + case 3: + ctx.setReturn(windowGetWidth(window)); + break; + case 4: + ctx.setReturn(windowGetHeight(window)); + break; + case 5: + ctx.setReturn(window); + break; + default: + ctx.setReturn(0); + break; + } +} + void mf_intface_redraw(OpcodeContext& ctx) { if (ctx.numArgs() == 0) { @@ -341,6 +539,11 @@ void mf_intface_redraw(OpcodeContext& ctx) } } +void mf_inventory_redraw(OpcodeContext& ctx) +{ + inventoryRedraw(ctx.numArgs() > 0 ? ctx.arg(0).asInt() : -1); +} + void mf_item_weight(OpcodeContext& ctx) { Object* object = ctx.arg(0).asObject(); @@ -355,7 +558,7 @@ void mf_item_weight(OpcodeContext& ctx) void mf_loot_obj(OpcodeContext& ctx) { - if (GameMode::isInGameMode(GameMode::kInventory)) { + if (GameMode::isInGameMode(GameMode::kLoot)) { ctx.setReturn(inventoryGetTargetObject()); } else { ctx.setReturn(nullptr); @@ -524,6 +727,41 @@ void mf_set_outline(OpcodeContext& ctx) object->outline = outline; } +void mf_set_window_flag(OpcodeContext& ctx) +{ + int bitFlag = ctx.arg(1).asInt(); + switch (bitFlag) { + case WINDOW_DONT_MOVE_TOP: + case WINDOW_MOVE_ON_TOP: + case WINDOW_HIDDEN: + case WINDOW_MODAL: + case WINDOW_TRANSPARENT: + break; + default: + return; + } + + bool enabled = ctx.arg(2).asInt() != 0; + if (ctx.arg(0).isString()) { + const char* windowName = ctx.stringArg(0); + if (!scriptWindowSetNamedFlag(windowName, bitFlag, enabled)) { + ctx.printError("%s() - window '%s' is not found.", ctx.name(), windowName); + } + return; + } + + int windowId = ctx.arg(0).asInt(); + if (windowId <= 0) { + windowId = getCurrentInterfaceWindow(); + } + + if (windowId == -1) { + return; + } + + applyWindowFlag(windowId, bitFlag, enabled); +} + void mf_set_unique_id(OpcodeContext& ctx) { Object* object = ctx.arg(0).asObject(); @@ -552,6 +790,18 @@ void mf_show_window(OpcodeContext& ctx) } } +void mf_hide_window(OpcodeContext& ctx) +{ + if (ctx.numArgs() == 0) { + scriptWindowHide(); + } else { + const char* windowName = ctx.stringArg(0); + if (!scriptWindowHideNamed(windowName)) { + ctx.printError("%s() - window '%s' is not found.", ctx.name(), windowName); + } + } +} + void mf_tile_refresh_display(OpcodeContext& ctx) { tileWindowRefresh(); diff --git a/src/sfall_opcodes.cc b/src/sfall_opcodes.cc index 4f6eaf99..3fe16f44 100644 --- a/src/sfall_opcodes.cc +++ b/src/sfall_opcodes.cc @@ -42,6 +42,7 @@ #include "stat.h" #include "svga.h" #include "tile.h" +#include "window_manager.h" #include "worldmap.h" namespace fallout { @@ -899,6 +900,11 @@ static void op_get_mouse_buttons(Program* program) programStackPushInteger(program, mouse_get_last_buttons()); } +static void op_get_window_under_mouse(Program* program) +{ + programStackPushInteger(program, _win_last_button_winID()); +} + // get_screen_width static void op_get_screen_width(Program* program) { @@ -1873,6 +1879,7 @@ void sfallOpcodesInit() // 0x821e - int get_mouse_buttons() interpreterRegisterOpcode(0x821E, op_get_mouse_buttons); // 0x821f - int get_window_under_mouse() + interpreterRegisterOpcode(0x821F, op_get_window_under_mouse); // 0x8163 - int get_year() interpreterRegisterOpcode(0x8163, op_get_year); diff --git a/src/skilldex.cc b/src/skilldex.cc index c552ce05..78494eeb 100644 --- a/src/skilldex.cc +++ b/src/skilldex.cc @@ -95,7 +95,7 @@ static MessageList gSkilldexMessageList; static MessageListItem gSkilldexMessageListItem; // 0x668140 skldxwin -static int gSkilldexWindow; +static int gSkilldexWindow = -1; // 0x668144 winbuf_2 static unsigned char* gSkilldexWindowBuffer; @@ -147,6 +147,11 @@ int skilldexOpen() return rc; } +int skilldexGetWindow() +{ + return windowGetWindow(gSkilldexWindow) != nullptr ? gSkilldexWindow : -1; +} + // 0x4AC054 skilldex_start static int skilldexWindowInit() { diff --git a/src/skilldex.h b/src/skilldex.h index 3c1b65c7..b0952310 100644 --- a/src/skilldex.h +++ b/src/skilldex.h @@ -17,6 +17,7 @@ typedef enum SkilldexRC { } SkilldexRC; int skilldexOpen(); +int skilldexGetWindow(); } // namespace fallout diff --git a/src/window.cc b/src/window.cc index ed5f183a..9f00045a 100644 --- a/src/window.cc +++ b/src/window.cc @@ -158,9 +158,6 @@ static ManagedWindow gManagedWindows[MANAGED_WINDOW_COUNT]; // 0x672D70 inputFunc static WindowInputHandler** gWindowInputHandlers; -// 0x672D74 createWindowFunc -static ManagedWindowCreateCallback* off_672D74; - // NOTE: This value is never set. // // 0x672D78 selectWindowFunc @@ -704,6 +701,50 @@ bool scriptWindowShow() return true; } +bool scriptWindowHideNamed(const char* windowName) +{ + for (int index = 0; index < MANAGED_WINDOW_COUNT; index++) { + ManagedWindow* managedWindow = &(gManagedWindows[index]); + if (managedWindow->window != -1) { + if (compat_stricmp(managedWindow->name, windowName) == 0) { + windowHide(managedWindow->window); + return true; + } + } + } + + return false; +} + +bool scriptWindowSetFlag(int windowId, int bitFlag, bool enabled) +{ + if (!windowIsValidWindowId(windowId) || windowId == 0) { + return false; + } + + Window* window = windowGetWindow(windowId); + if (window == nullptr) { + return false; + } + + if (bitFlag == WINDOW_HIDDEN) { + if (enabled) { + windowHide(windowId); + } else { + windowShow(windowId); + } + return true; + } + + if (enabled) { + window->flags |= bitFlag; + } else { + window->flags &= ~bitFlag; + } + + return true; +} + // 0x4B7734 int scriptWindowWidth() { @@ -843,9 +884,6 @@ int scriptWindowCreate(const char* windowName, int x, int y, int width, int heig managedWindow->buttonsLength = 0; flags |= WINDOW_MANAGED | WINDOW_USE_DEFAULTS; - if (off_672D74 != nullptr) { - off_672D74(windowIndex, managedWindow->name, &flags); - } managedWindow->window = windowCreate(x, y, width, height, a6, flags); managedWindow->cursorY = 0; @@ -950,23 +988,30 @@ unsigned char* scriptWindowGetBuffer() // 0x4B8330 int scriptWindowPush(const char* windowName) { - if (_winTOS >= MANAGED_WINDOW_COUNT) { - return -1; + int oldCurrentWindowIndex = gCurrentManagedWindowIndex; + + int existingIndex = -1; + for (int index = 0; index <= _winTOS; index++) { + if (_winStack[index] == oldCurrentWindowIndex) { + existingIndex = index; + break; + } } - int oldCurrentWindowIndex = gCurrentManagedWindowIndex; + // CE: check duplicates before checking capacity; removing one frees a stack slot. + int newTopIndex = existingIndex == -1 ? _winTOS + 1 : _winTOS; + if (newTopIndex >= MANAGED_WINDOW_COUNT) { + return -1; + } int windowIndex = scriptWindowSelect(windowName); if (windowIndex == -1) { return -1; } - // TODO: Check. - for (int index = 0; index < _winTOS; index++) { - if (_winStack[index] == oldCurrentWindowIndex) { - memcpy(&(_winStack[index]), &(_winStack[index + 1]), sizeof(*_winStack) * (_winTOS - index)); - break; - } + if (existingIndex != -1) { + memmove(&(_winStack[existingIndex]), &(_winStack[existingIndex + 1]), sizeof(*_winStack) * (_winTOS - existingIndex)); + _winTOS--; } _winTOS++; @@ -983,9 +1028,15 @@ int _popWindow() } int windowIndex = _winStack[_winTOS]; - ManagedWindow* managedWindow = &(gManagedWindows[windowIndex]); _winTOS--; + if (windowIndex < 0 || windowIndex >= MANAGED_WINDOW_COUNT) { + gCurrentManagedWindowIndex = -1; + return -1; + } + + ManagedWindow* managedWindow = &(gManagedWindows[windowIndex]); + return scriptWindowSelect(managedWindow->name); } @@ -1745,8 +1796,7 @@ bool scriptWindowAddButtonGfx(const char* buttonName, char* pressedFileName, cha } if (hoverFileName != nullptr) { - // TODO: this almost certainly should be hoverFileName - unsigned char* hover = datafileRead(normalFileName, &width, &height); + unsigned char* hover = datafileRead(hoverFileName, &width, &height); if (hover != nullptr) { if (managedButton->hover == nullptr) { managedButton->hover = (unsigned char*)internal_malloc_safe(managedButton->height * managedButton->width, __FILE__, __LINE__); // "..\\int\\WINDOW.C, 1849 @@ -2657,4 +2707,19 @@ bool scriptWindowShowNamed(const char* windowName) return false; } +bool scriptWindowSetNamedFlag(const char* windowName, int bitFlag, bool enabled) +{ + for (int index = 0; index < MANAGED_WINDOW_COUNT; index++) { + ManagedWindow* managedWindow = &(gManagedWindows[index]); + if (managedWindow->window != -1) { + if (compat_stricmp(managedWindow->name, windowName) == 0) { + // note: Sfall also updates managedWindow->flags, but that is unused + return scriptWindowSetFlag(managedWindow->window, bitFlag, enabled); + } + } + } + + return false; +} + } // namespace fallout diff --git a/src/window.h b/src/window.h index 50b69633..6d776e5e 100644 --- a/src/window.h +++ b/src/window.h @@ -65,6 +65,7 @@ void _doRightButtonRelease(int btn, int keyCode); void _setButtonGFX(int width, int height, unsigned char* normal); bool scriptWindowHide(); bool scriptWindowShow(); +bool scriptWindowHideNamed(const char* name); int scriptWindowWidth(); int scriptWindowHeight(); bool scriptWindowDraw(); @@ -131,6 +132,8 @@ void _drawScaledBuf(unsigned char* dest, int destWidth, int destHeight, unsigned void _fillBuf3x3(unsigned char* src, int srcWidth, int srcHeight, unsigned char* dest, int destWidth, int destHeight); bool scriptWindowShowNamed(const char* name); +bool scriptWindowSetFlag(int windowId, int bitFlag, bool enabled); +bool scriptWindowSetNamedFlag(const char* name, int bitFlag, bool enabled); } // namespace fallout diff --git a/src/window_manager.cc b/src/window_manager.cc index 0422a620..30b67db3 100644 --- a/src/window_manager.cc +++ b/src/window_manager.cc @@ -1121,9 +1121,14 @@ void _refresh_all(Rect* rect, unsigned char* dest) } // 0x4D7888 +bool windowIsValidWindowId(int win) +{ + return win >= 0 && win < MAX_WINDOW_COUNT; +} + Window* windowGetWindow(int win) { - if (win == -1) { + if (!windowIsValidWindowId(win)) { return nullptr; } diff --git a/src/window_manager.h b/src/window_manager.h index 2abffc80..91b1ded6 100644 --- a/src/window_manager.h +++ b/src/window_manager.h @@ -203,6 +203,7 @@ void windowRefreshRect(int win, const Rect* rect); void _GNW_win_refresh(Window* window, Rect* rect, unsigned char* dest); void windowRefreshAll(Rect* rect); void _win_get_mouse_buf(unsigned char* dest); +bool windowIsValidWindowId(int win); Window* windowGetWindow(int win); unsigned char* windowGetBuffer(int win); Buffer2D windowGetBuffer2D(int win); diff --git a/src/worldmap.cc b/src/worldmap.cc index 714d29c9..fc5f54b7 100644 --- a/src/worldmap.cc +++ b/src/worldmap.cc @@ -690,6 +690,11 @@ static int wmMaxMapNum = 0; // 0x51DE14 wmBkWin static int wmBkWin = -1; +int worldmapGetWindow() +{ + return windowGetWindow(wmBkWin) != nullptr ? wmBkWin : -1; +} + // 0x51DE24 wmBkWinBuf static unsigned char* wmBkWinBuf = nullptr; diff --git a/src/worldmap.h b/src/worldmap.h index de79e2d1..b0877f55 100644 --- a/src/worldmap.h +++ b/src/worldmap.h @@ -289,6 +289,7 @@ void wmSetPartyWorldPos(int x, int y); void wmCarSetCurrentArea(int area); void wmForceEncounter(int map, unsigned int flags); void wmSetScriptWorldMapMulti(float value); +int worldmapGetWindow(); } // namespace fallout