Compare commits

...
Author SHA1 Message Date
Mike Klaas b9936e10ff PR 2026-03-31 11:51:16 -07:00
Mike Klaas 6bd1daae0b include 2026-03-31 11:30:47 -07:00
Mike Klaas d8336761cc fixes 2026-03-31 11:25:08 -07:00
Mike Klaas 5055e2b0d4 Path normalization fixex 2026-03-31 10:09:44 -07:00
Mike Klaas a0954c8df7 cleanup 2026-03-31 10:09:44 -07:00
Mike Klaas 7abab85e0a Normalize global script archive paths 2026-03-31 10:09:44 -07:00
Vlad K 511d53a615 fix set_proto_data leftover value on stack in case of invalid args + use programPrintError (#344) 2026-03-31 17:03:50 +02:00
Mike Klaas f17dd19116 Fix parameter order in metarules set_flags and set_outline. (#341)
* Fix parameter order in metarules set_flags and set_outline.

Also fix missing return val in message_box error path
2026-03-30 08:35:35 -07:00
Mike Klaas c28c62727f Implement sfall animation opcodes and metarule (#326) 2026-03-30 08:24:57 -07:00
Mike Klaas b14f518714 Comment out hires_stencil debug logs (#339)
These are a bit too much when debugging other features.  Could make this controlled by a bool too if people prefer
2026-03-30 07:53:57 -07:00
Vlad K 6a0bed4433 Death Anim script hook + fall anim fix + related refactoring (#336)
* actions: refactor code around death anim selection

* add Death Anim hook from sfall

* AI review fixes

* Rename constants for force field PID/FID
2026-03-30 00:23:53 +02:00
Mike Klaas ce27eb55a1 Sfall compatibility tracker (#334)
The idea is that modders / contributors could track what is implemented, what is planned/unplanned, &c.  There's a risk that we won't keep it up to date, but I think it's a document that we'll need eventually.
2026-03-29 13:32:45 -07:00
Mike Klaas c240657d0d Fix two metarule argument popping order errors (#335)
* Fix two metarule argument popping order errors

Also, backfill all errorReturn and argument type validation for metarules
2026-03-29 13:26:34 -07:00
Vlad Kandgithub-actions[bot] 0a73abe67d Barter hook refactoring and Keypress hook repeat fix (#333)
* Barter hook refactor: weight compute in separate fn, names and comments for related vars, consistent value args

* Hook scripts: use initializer_list for defining args

* Keypress Hook: don't trigger on repeat, to match sfall

* chore: auto-format with clang-format

* Attempt to fix formatting after clang-format

* Don't use references for return values, for better readability

* chore: auto-format with clang-format

* Code review fixes

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-29 13:57:30 +02:00
Vlad K 54228248ef Add barter hook from sfall (#331)
* Add barter hook from sfall

* Code review fixes
2026-03-28 10:43:47 +01:00
Mike Klaas 4eba423399 Use GameMode::getCurrentGameMode() instead of custom game mode detection (#328)
* Use `GameMode::getCurrentGameMode()` instead of custom game mode detection
2026-03-28 00:41:33 -07:00
Mike Klaas de98136699 Implement sfall tile/path opcodes (#330)
* Implementation of tile and path opcodes
2026-03-27 22:58:29 -07:00
Mike Klaas b27b0f03fc Fix get_ini_setting (#329)
Previously it was returning `0` if the key is missing from the ini file, but the spec is to return -1: https://sfall-team.github.io/sfall/ini-settings/#get_ini_setting

Note that if the key _exists_, but is the value is empty (`key=`), sfall returns `0`.
2026-03-26 07:51:00 -07:00
Mike Klaas 9d6f86424e Implement sfall add_extra_message_file (#327)
https://sfall-team.github.io/sfall/sfall-funcx-macros/#add_extra_msg_file

For now we're choosing to not support the 2-arg form.  We could return `-1` if the script tries to do this, but if so and it doesn't check for an error, that's likely worse than just crashing at this point.  It's essentially an unimplemented opcode.
2026-03-25 21:19:35 -07:00
Mike Klaas bb65d8d32a sfall opcode refresh_pc_art (#324)
Needed for RPU
2026-03-24 05:30:08 +00:00
Mike Klaas c9139f762d Deobfuscate scripts.cc/h (#323)
* Deobfuscate `scripts.cc/h`

Mostly renaming, but also includes:
 * Some straightforward uninling
 * Function extraction of a pretty large chunk (elevators)
 * Simplification of the script loading loop

Tested manually
2026-03-22 04:25:46 +00:00
Mike Klaas beb3c537a4 Fix several compile warnings (#316)
* Fix several compile warnings

Main change is making display functions accept const char* instead of mutating their arguments.
2026-03-21 21:42:15 +00:00
NovaRain b71f1e7d18 Merge pull request #313 from NovaRain/wmenc_table_fix
Fix crash/glitched map when the encounter table has no available entries
2026-03-21 18:33:15 +08:00
Vlad K b7f2b32b92 Debug window size setting, start debug output earlier and log number of loaded mods (#321)
* Debug window size setting, start debug output earlier and log total number of mods loaded

* Code review fixes
2026-03-21 10:54:32 +01:00
Vlad K 75776e4b5b HOOK_COMBATDAMAGE (#322)
* HOOK_COMBATDAMAGE

* Fix compilation on 64bit systems

* Fix incorrect hit location var
2026-03-20 10:53:03 -07:00
Mike Klaas 5304823f8f Fix argument order in mapper: mapSetEnteringLocation (#315)
Mapper's call of this function uses the wrong order compared to all other uses

Fixes #294
2026-03-20 13:08:09 +01:00
NovaRain 23e9eab72d Fix crash/glitched map when the encounter table has no available entries (#311) 2026-03-19 09:13:43 +08:00
65 changed files with 2172 additions and 924 deletions
+2
View File
@@ -289,6 +289,8 @@ target_sources(${EXECUTABLE_NAME} PUBLIC
"src/sfall_script_hooks.cc"
"src/sfall_arrays.cc"
"src/sfall_arrays.h"
"src/sfall_animation.cc"
"src/sfall_animation.h"
"src/sfall_callbacks.cc"
"src/sfall_callbacks.h"
"src/touch.cc"
+104
View File
@@ -0,0 +1,104 @@
# Sfall Compatibility
This document tracks Fallout 2 CE compatibility with sfall. This is for modders who need to know which Sfall features work in CE.
For now, this covers opcodes/metarules, and hooks. In the future, it will include other ways of modifying the engine (like ini files), and other Sfall-specific behaviour.
## Opcodes / Metarules
See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/) for documentation on specific functions.
| Group | Opcodes In Group | Compatibility | Notes |
| --- | --- | --- | --- |
| Direct memory access| read_byte,short,int,string<br>write_byte,short,int,string<br>call_offset_vX | đźš« | Not possible. Open an issue if you need functionality not covered by other opcodes. |
| Stats | get/set_pc_base_stat<br>get/set_pc_extra_stat<br>get/set_critter_base_stat<br>get/set_critter_extra_stat | âś… | CE uses engine stat helpers here instead of sfall's direct proto-field behavior, so derived-stat update behavior can differ. |
| Stats / Alter min/max | get/set_stat_min/max<br>set_pc_stat_min/max<br>set_npc_stat_min/max | not implemented | - |
| Skills | get/set_critter_skill_points<br>get/set_available_skill_points<br>set_skill_max<br>set_critter_skill_mod<br>set_base_skill_mod<br>mod_skill_points_per_level | not implemented | - |
| Graphics | graphics_funcs_available<br>force_graphics_refresh<br>get_screen_width<br>get_screen_height<br>set_palette | implemented: only get_screen_width, get_screen_height | - |
| Shaders | load_shader<br>free_shader<br>activate_shader<br>deactivate_shader<br>set/get_shader_* | đźš« | likely will not implement direct compatibility
| Perks and traits | set_perk_image<br>set_perk_*<br>set_pyromaniac_mod<br>apply_heaveho_fix<br>set_swiftlearner_mod<br>set_fake_perk<br>set_fake_trait<br>set_selectable_perk<br>set_perkbox_title<br>hide_real_perks<br>show_real_perks<br>perk_add_mode<br>clear_selectable_perks<br>has_fake_perk<br>has_fake_trait<br>add_trait<br>remove_trait<br>seq_perk_freq | not implemented | - |
| Virtual file system | fs_create<br>fs_copy<br>fs_find<br>fs_read/write_*<br>fs_delete<br>fs_size<br>fs_pos<br>fs_seek<br>fs_resize | đźš« | Open an issue if you have a use case for these |
| Combat / Knockback | set_weapon_knockback<br>set_target_knockback<br>set_attacker_knockback<br>remove_weapon_knockback<br>remove_target_knockback<br>remove_attacker_knockback | not implemented | - |
| Maps and encounters | in_world_map<br>force_encounter<br>force_encounter_with_flags<br>set_map_time_multi<br>get/set_map_enter_position<br>exec_map_update_scripts<br>get/set_terrain_name<br>set_town_title<br>get/set_can_rest_on_map<br>set_rest_heal_time<br>set_rest_mode<br>set_worldmap_heal_time | implemented: in_world_map, force_encounter, force_encounter_with_flags, set_map_time_multi | - |
| Maps and encounters / Worldmap | get_world_map_x/y_pos<br>set_world_map_pos | âś… | - |
| Audio | eax_available<br>set_eax_environment<br>play_sfall_sound<br>stop_sfall_sound | not implemented | *eax* opcodes will not be implemented |
| Combat / Weapons and ammo | get/set_weapon_ammo_pid<br>get/set_weapon_ammo_count | âś… | - |
| Sfall / Version | sfall_ver_major<br>sfall_ver_minor<br>sfall_ver_build | âś… | CE currently reports `4.3.4` |
| Utility / Math | log, exponent, round, sqrt, abs, sin, cos, tan, arctan, ceil, ^, floor2, div | âś… | - |
| Keyboard and mouse | key_pressed<br>tap_key<br>get_mouse_x/y<br>get_mouse_buttons | âś… | - |
| Lists | list_begin<br>list_next<br>list_end<br>list_as_array<br>party_member_list | âś… | - |
| Explosions | metarule2_explosions<br>set_attack_explosion_pattern<br>set_attack_explosion_art<br>set_attack_explosion_radius<br>set_attack_is_explosion_fire<br>set_explosion_radius<br>set_dynamite_damage<br>set_plastic_damage<br>get_explosion_damage<br>set_explosion_max_targets<br>item_make_explosive | âś… except item_make_explosive | - |
| Animations | reg_anim_combat_check<br>reg_anim_destroy<br>reg_anim_animate_and_hide<br>reg_anim_light<br>reg_anim_change_fid<br>reg_anim_take_out<br>reg_anim_turn_towards<br>reg_anim_callback<br>reg_anim_animate_and_move | âś… except reg_anim_callback | - |
| Art and appearance | art_exists<br>refresh_pc_art<br>art_cache_clear<br>set_hero_race<br>set_hero_style | implemented: art_exists, refresh_pc_art, art_cache_clear | - |
| Tiles and paths | get_tile_fid<br>tile_under_cursor<br>tile_light<br>tile_get_objs<br>tile_refresh_display<br>obj_blocking_tile<br>tile_by_position<br>get_tile_ground_fid<br>get_tile_roof_fid<br>obj_blocking_line<br>path_find_to<br>objects_in_radius | âś… except objects_in_radius | - |
| Utility | sprintf<br>typeof<br>atoi<br>atof | âś… | - |
| Utility / Strings | string_split<br>substr<br>strlen<br>charcode<br>get_string_pointer<br>string_find<br>string_find_from<br>string_format<br>string_format_array<br>string_replace<br>string_to_case<br>string_compare | âś… except get_string_pointer | `get_string_pointer` is deprecated and intentionally omitted. |
| Interface / Tags | show_iface_tag<br>hide_iface_tag<br>is_iface_tag_active<br>set_iface_tag_text<br>add_iface_tag | implemented: all except set_iface_tag_text, add_iface_tag | CE only handles built-in interface tags here; custom tag creation/text is not supported yet. |
| Global variables | set_sfall_global<br>get_sfall_global_int<br>get_sfall_global_float | implemented: all except get_sfall_global_float | Current CE storage is int-backed; `set_sfall_global` stores integer values and there is no float getter yet. |
| Hooks / Hook functions | init_hook<br>get_sfall_arg<br>get_sfall_args<br>get_sfall_arg_at<br>set_sfall_return<br>set_sfall_arg<br>register_hook<br>register_hook_proc<br>register_hook_proc_spec | âś… except init_hook | See below for implemented hooks. `init_hook` is deprecated and will not be implemented. register_hook_proc and register_hook_proc_spec both add hooks to the *end* of the hook list, instead of beginning and end, respectively. |
| Arrays / Array functions | create_array<br>temp_array<br>fix_array<br>get/set_array<br>resize_array<br>free_array<br>scan_array<br>len_array<br>save/load_array<br>array_key<br>arrayexpr | âś… except save_array, load_array | - |
| Perks and traits / NPC perks | set_fake_perk_npc<br>set_fake_trait_npc<br>set_selectable_perk_npc<br>has_fake_perk_npc<br>has_fake_trait_npc | not implemented | - |
| Global scripts / Global script functions | set_global_script_repeat<br>set_global_script_type<br>available_global_script_types | implemented: all except available_global_script_types | `GlobalScriptPaths` masks now match archive-backed scripts regardless of `/` vs `\` separators, matching Windows-style `.dat` contents on non-Windows hosts. |
| Combat | attack_is_aimed<br>block_combat<br>force_aimed_shots<br>disable_aimed_shots<br>get_attack_type<br>get/set_bodypart_hit_modifier<br>combat_data<br>get/set/reset_critical_table<br>get_last_target<br>get_last_attacker<br>set_critter_burst_disable<br>get/set_critter_current_ap<br>set_spray_settings<br>get/set_combat_free_move | implemented: only get_attack_type, get_bodypart_hit_modifier, combat_data, set_bodypart_hit_modifier | - |
| Car | set_car_current_town<br>car_gas_amount<br>set_car_intface_art | implemented: all except set_car_intface_art | - |
| Interface / Windows and images | art_frame_data<br>interface_art_draw<br>interface_print<br>draw_image<br>draw_image_scaled<br>get_window_under_mouse<br>create_win<br>get_window_attribute<br>message_box<br>set_window_flag<br>win_fill_color<br>interface_overlay<br>dialog_message<br>get_text_width<br>hide_window<br>show_window | implemented: only message_box, get_text_width, show_window | - |
| Interface / Outline | outlined_object<br>get_outline<br>set_outline | implemented: all except get_outline | - |
| Interface / Main interface | intface_is_hidden<br>intface_redraw<br>intface_hide<br>intface_show<br>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<br>inventory_redraw<br>critter_inven_obj2<br>get_current_inven_size<br>item_weight | implemented: only critter_inven_obj2 | - |
| Interface / Cursor | get/set_cursor_mode | âś… | - |
| Locks | lock_is_jammed<br>unjam_lock<br>set_unjam_locks_time | not implemented | - |
| INI settings | modified_ini<br>get_ini_setting<br>get_ini_string<br>get_ini_section<br>get_ini_sections<br>get_ini_config<br>get_ini_config_db<br>set_ini_setting | âś… except modified_ini, get_ini_config, get_ini_config_db | `modified_ini` is intentionally omitted as deprecated. |
| Objects and scripts | set_self<br>set_dude_obj<br>real_dude_obj<br>remove_script<br>set_script<br>get_script<br>obj_is_carrying_obj<br>loot_obj<br>dialog_obj<br>obj_under_cursor<br>get_object_data<br>set_object_data<br>get_flags<br>set_flags<br>set_unique_id<br>set_scr_name<br>obj_is_openable<br>get/set_proto_data<br>get_object_ai_data | implemented: set_self, get_script, loot_obj, dialog_obj, obj_under_cursor, get_object_data, get_flags, set_flags, get_proto_data, set_proto_data | - |
| Other / Game management | set_movie_path<br>stop_game<br>resume_game<br>mark_movie_played<br>game_loaded<br>get_game_mode<br>get_uptime<br>signal_close_game | implemented: game_loaded, get_game_mode, get_uptime | - |
| Other | input_funcs_available<br>get_year<br>set_dm_model<br>set_df_model<br>set_pipboy_available<br>get_kill_counter<br>mod_kill_counter<br>active_hand<br>toggle_active_hand<br>set_pickpocket_max<br>set_hit_chance_max<br>set_xp_mod<br>set_critter_hit_chance_mod<br>set_base_hit_chance_mod<br>inc_npc_level<br>get_npc_level<br>get/set_viewport_x/y<br>set_hp_per_level_mod<br>get/set_unspent_ap_bonus<br>get/set_unspent_ap_perk_bonus<br>hero_select_win<br>create_message_window<br>get_light_level<br>gdialog_get_barter_mod<br>set_inven_ap_cost<br>set_base_pickpocket_mod<br>set_critter_pickpocket_mod<br>message_str_game<br>sneak_success<br>create_spatial<br>unwield_slot<br>get_inven_ap_cost<br>add_g_timer_event<br>add_extra_msg_file<br>get_metarule_table<br>metarule_exist<br>npc_engine_level_up<br>remove_timer_event<br>set_drugs_data<br>spatial_radius | implemented: get_year, active_hand, toggle_active_hand, create_message_window, message_str_game, add_extra_msg_file, metarule_exist | `input_funcs_available`, `nb_create_char` are deprecated in sfall and intentionally absent in CE. `add_extra_msg_file` does not support the explicit `fileNumber` form in CE. |
## Hooks
| Hook | ID | Compatibility | Notes |
| --- | --- | --- | --- |
| ToHit | `HOOK_TOHIT` | âś… | - |
| AfterHitRoll | `HOOK_AFTERHITROLL` | đźš« | Et tu |
| CalcAPCost | `HOOK_CALCAPCOST` | đźš« | - |
| DeathAnim1 | `HOOK_DEATHANIM1` | đźš« | Use DEATHANIM2 instead |
| DeathAnim2 | `HOOK_DEATHANIM2` | âś… | - |
| CombatDamage | `HOOK_COMBATDAMAGE` | âś… | CE passes the raw `Attack*` as the final mixed argument, matching sfall's shape. |
| OnDeath | `HOOK_ONDEATH` | đźš« | - |
| FindTarget | `HOOK_FINDTARGET` | đźš« | (maybe) |
| UseObjOn | `HOOK_USEOBJON` | âś… | - |
| UseObj | `HOOK_USEOBJ` | âś… | CE notes an sfall-matching inconsistency around return code `2` behavior between interface contexts. |
| RemoveInvenObj | `HOOK_REMOVEINVENOBJ` | đźš« | - |
| BarterPrice | `HOOK_BARTERPRICE` | âś… | - |
| MoveCost | `HOOK_MOVECOST` | đźš« | - |
| ItemDamage | `HOOK_ITEMDAMAGE` | đźš« | - |
| AmmoCost | `HOOK_AMMOCOST` | đźš« | Et tu |
| KeyPress | `HOOK_KEYPRESS` | âś… | Third hook arg is currently `0`; CE notes that sfall used VK codes there. |
| MouseClick | `HOOK_MOUSECLICK` | đźš« | - |
| UseSkill | `HOOK_USESKILL` | đźš« | - |
| Steal | `HOOK_STEAL` | đźš« | Et tu |
| WithinPerception | `HOOK_WITHINPERCEPTION` | đźš« | Et tu |
| InventoryMove | `HOOK_INVENTORYMOVE` | đźš« | Et tu |
| InvenWield | `HOOK_INVENWIELD` | đźš« | - |
| AdjustFID | `HOOK_ADJUSTFID` | đźš« | - |
| CombatTurn | `HOOK_COMBATTURN` | đźš« | - |
| StdProcedure | `HOOK_STDPROCEDURE` | đźš« | Et tu |
| StdProcedureEnd | `HOOK_STDPROCEDURE_END` | đźš« | - |
| CarTravel | `HOOK_CARTRAVEL` | đźš« | - |
| SetGlobalVar | `HOOK_SETGLOBALVAR` | đźš« | - |
| RestTimer | `HOOK_RESTTIMER` | đźš« | Et tu |
| GameModeChange | `HOOK_GAMEMODECHANGE` | âś… | - |
| UseAnimObj | `HOOK_USEANIMOBJ` | đźš« | Et tu; (maybe) |
| ExplosiveTimer | `HOOK_EXPLOSIVETIMER` | đźš« | - |
| DescriptionObj | `HOOK_DESCRIPTIONOBJ` | đźš« | Et tu |
| UseSkillOn | `HOOK_USESKILLON` | đźš« | Et tu |
| OnExplosion | `HOOK_ONEXPLOSION` | đźš« | (maybe) |
| SubCombatDamage | `HOOK_SUBCOMBATDAMAGE` | đźš« | (maybe) |
| SetLighting | `HOOK_SETLIGHTING` | đźš« | Et tu; (maybe) |
| Sneak | `HOOK_SNEAK` | đźš« | - |
| TargetObject | `HOOK_TARGETOBJECT` | đźš« | (maybe) |
| Encounter | `HOOK_ENCOUNTER` | đźš« | Et tu |
| AdjustPoison | `HOOK_ADJUSTPOISON` | đźš« | (maybe) |
| AdjustRads | `HOOK_ADJUSTRADS` | đźš« | (maybe) |
| RollCheck | `HOOK_ROLLCHECK` | đźš« | - |
| BestWeapon | `HOOK_BESTWEAPON` | đźš« | - |
| CanUseWeapon | `HOOK_CANUSEWEAPON` | đźš« | - |
| BuildSfxWeapon | `HOOK_BUILDSFXWEAPON` | đźš« | - |
+56
View File
@@ -0,0 +1,56 @@
#include "sfall.h"
#include "dik.h"
#include "define_lite.h"
#include "define_extra.h"
#include "test_utils.h"
variable refresh_pc_art_test_done := false;
procedure run_refresh_pc_art_test begin
variable old_gender := get_critter_stat(dude_obj, STAT_gender);
variable new_gender := 1 - old_gender;
variable dude_pid := obj_pid(dude_obj);
variable old_proto_fid := get_proto_data(dude_pid, PROTO_FID);
set_pc_base_stat(STAT_gender, new_gender);
call assertEquals("gender changes immediately", get_critter_stat(dude_obj, STAT_gender), new_gender);
call assertEquals("proto fid unchanged before refresh", get_proto_data(dude_pid, PROTO_FID), old_proto_fid);
refresh_pc_art;
variable refreshed_proto_fid := get_proto_data(dude_pid, PROTO_FID);
call assertNotEquals("refresh_pc_art updates dude proto fid", refreshed_proto_fid, old_proto_fid);
if (not critter_inven_obj(dude_obj, INVEN_TYPE_WORN)) then begin
call assertEquals("refresh_pc_art updates unarmored dude art", obj_art_fid(dude_obj) bwand 0xFFF, refreshed_proto_fid bwand 0xFFF);
end
set_pc_base_stat(STAT_gender, old_gender);
refresh_pc_art;
call assertEquals("refresh_pc_art restores original proto fid", get_proto_data(dude_pid, PROTO_FID), old_proto_fid);
refresh_pc_art_test_done := true;
call report_test_results("hook_refresh_pc_art");
end
procedure keypress_handler begin
variable pressed := get_sfall_arg_at(0);
variable key := get_sfall_arg_at(1);
if (not pressed) then return;
if (key != DIK_X) then return;
if (refresh_pc_art_test_done) then begin
display_msg("refresh_pc_art test already ran");
return;
end
call run_refresh_pc_art_test;
end
procedure start begin
if (not game_loaded) then return;
display_msg("refresh_pc_art test ready: press X");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
end
@@ -0,0 +1,26 @@
#include "test_utils.h"
#include "sfall.h"
// to test, copy test_extra_msg_file.msg into <game folder>/text/English/game/
procedure start begin
variable auto_id;
variable duplicate_auto_id;
display_msg("Testing add_extra_msg_file...");
call assertEquals("metarule exists", metarule_exist("add_extra_msg_file"), 1);
auto_id := add_extra_msg_file("test_extra_msg_file.msg");
call assertNotEquals("auto id is not load error", auto_id, -2);
call assertTrue("auto id lower bound", auto_id >= 0x3000);
call assertTrue("auto id upper bound", auto_id <= 0x3FFF);
call assertEquals("auto message", message_str_game(auto_id, 100), "auto extra msg");
duplicate_auto_id := add_extra_msg_file("TEST_EXTRA_MSG_FILE.MSG");
call assertEquals("duplicate file returns existing id", duplicate_auto_id, auto_id);
call assertEquals("missing file", add_extra_msg_file("missing_extra_msg_file.msg"), -2);
call report_test_results("add_extra_msg_file");
end
+10 -6
View File
@@ -9,9 +9,10 @@ procedure ini_test_suite begin
// Test Case 1: Valid file, valid section
result_array := get_ini_section("test.ini", "ValidSection");
expected_array := {"Key1": "Value1", "Key2": "2"};
call assertEquals("TC1 Size", len_array(result_array), 2);
call assertEquals("TC1 Content", arrays_equal(result_array, expected_array), true);
call assertEquals("TC1 Size", len_array(result_array), 3);
call assertEquals("TC1 Key1", result_array["Key1"], "Value1");
call assertEquals("TC1 Key2", result_array["Key2"], "2");
call assertEquals("TC1 EmptyValue", result_array["EmptyValue"], "");
count := 0;
foreach key_val_pair in result_array begin
count += 1;
@@ -25,19 +26,22 @@ procedure ini_test_suite begin
call assertEquals("TC2 get_ini_string string", get_ini_string("test.ini|ValidSection|Key1"), "Value1");
call assertEquals("TC2 get_ini_setting int", get_ini_setting("test.ini|ValidSection|Key2"), 2);
call assertEquals("TC2 get_ini_string int", get_ini_string("test.ini|ValidSection|Key2"), "2");
call assertEquals("TC2 get_ini_setting empty int", get_ini_setting("test.ini|ValidSection|EmptyValue"), 0);
call assertEquals("TC2 get_ini_string empty string", get_ini_string("test.ini|ValidSection|EmptyValue"), "");
// INI file not found
result_array := get_ini_section("nonexistent.ini", "AnySection");
call assertEquals("TC3 Size", len_array(result_array), 0);
call assertEquals("TC3 Is Map", array_key(result_array, -1), 1); // Should still be an associative array
call assertEquals("TC3 get_ini_setting", get_ini_setting("notexist.ini|ValidSection|Key2"), 0);
call assertEquals("TC3 get_ini_setting missing file", get_ini_setting("notexist.ini|ValidSection|Key2"), -1);
call assertEquals("TC3 get_ini_setting missing key", get_ini_setting("test.ini|ValidSection|MissingKey"), -1);
call assertEquals("TC3 get_ini_string", get_ini_string("notexist.ini|ValidSection|Key2"), "");
// Valid file, section not found
result_array := get_ini_section("test.ini", "NonExistentSection");
call assertEquals("TC4 Size", len_array(result_array), 0);
call assertEquals("TC4 Is Map", array_key(result_array, -1), 1);
call assertEquals("TC4 get_ini_setting", get_ini_setting("test.ini|NonExistentSection|Key2"), 0);
call assertEquals("TC4 get_ini_setting", get_ini_setting("test.ini|NonExistentSection|Key2"), -1);
call assertEquals("TC4 get_ini_string", get_ini_string("test.ini|NonExistentSection|Key2"), "");
@@ -45,7 +49,7 @@ procedure ini_test_suite begin
result_array := get_ini_section("test.ini", "EmptySection");
call assertEquals("TC5 Size", len_array(result_array), 0);
call assertEquals("TC5 Is Map", array_key(result_array, -1), 1);
call assertEquals("TC5 get_ini_setting", get_ini_setting("test.ini|EmptySection|Key2"), 0);
call assertEquals("TC5 get_ini_setting", get_ini_setting("test.ini|EmptySection|Key2"), -1);
call assertEquals("TC5 get_ini_string", get_ini_string("test.ini|EmptySection|Key2"), "");
// set ini setting
@@ -0,0 +1,234 @@
#include "sfall.h"
#include "dik.h"
#include "command_lite.h"
#include "define_extra.h"
#include "test_utils.h"
#define REG_ANIM_BEGIN 1
#define REG_ANIM_END 3
#define ANIMATION_REQUEST_UNRESERVED 1
#define TEST_ITEM_PID 40
#define TEST_FID_SOURCE_PID 76
#define TEST_LIGHT_VALUE 0x12340002
#define TEST_TAKE_OUT_FRAME WPN_ANIM_PISTOL
#define TEST_STAND_ANIM 0
#define TEST_WALK_ANIM 1
#define reg_anim_begin() reg_anim_func(REG_ANIM_BEGIN, ANIMATION_REQUEST_UNRESERVED)
#define reg_anim_end() reg_anim_func(REG_ANIM_END, 0)
variable hook_registered := false;
variable pending_test := 0;
variable pending_frames := 0;
variable test_move_critter_obj := 0;
variable test_takeout_critter_obj := 0;
variable test_light_obj := 0;
variable test_fid_obj := 0;
variable test_fid_source_obj := 0;
variable test_hide_obj := 0;
variable test_destroy_obj := 0;
variable expected_turn_tile := 0;
variable expected_move_tile := 0;
variable initial_rotation := 0;
variable initial_tile := 0;
variable initial_fid := 0;
variable expected_fid := 0;
variable initial_hide_visibility := 0;
variable initial_destroy_visibility := 0;
variable test_objects_ready := false;
variable pending_timeout_frames := 0;
variable noncombat_test_completed := false;
// Manual checks:
// X:
// - No registered animation should run in combat.
// - The temp critters should not move or draw a weapon.
// V:
// - `reg_anim_combat_check(0)` should allow the registered animations in combat.
// N:
// - Outside combat, the full opcode surface should run and be checked.
procedure setup_test_objects begin
variable elev := elevation(dude_obj);
test_move_critter_obj := create_object_sid(obj_pid(dude_obj), tile_num_in_direction(dude_tile, dude_cur_rot, 3), elev, -1);
test_takeout_critter_obj := create_object_sid(obj_pid(dude_obj), tile_num_in_direction(dude_tile, (dude_cur_rot + 1) % 6, 3), elev, -1);
test_light_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 2) % 6, 4), elev, -1);
test_fid_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 3) % 6, 4), elev, -1);
test_fid_source_obj := create_object_sid(TEST_FID_SOURCE_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 4) % 6, 4), elev, -1);
test_hide_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 5) % 6, 4), elev, -1);
test_destroy_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, dude_cur_rot, 5), elev, -1);
initial_rotation := obj_get_rot(test_move_critter_obj);
initial_tile := tile_num(test_move_critter_obj);
expected_turn_tile := tile_num_in_direction(initial_tile, (initial_rotation + 3) % 6, 1);
expected_move_tile := tile_num_in_direction(initial_tile, initial_rotation, 1);
initial_fid := get_object_data(test_fid_obj, OBJ_DATA_FID);
expected_fid := get_object_data(test_fid_source_obj, OBJ_DATA_FID);
initial_hide_visibility := obj_is_visible_flag(test_hide_obj);
initial_destroy_visibility := obj_is_visible_flag(test_destroy_obj);
pending_frames := 0;
test_objects_ready := true;
end
procedure ensure_test_objects begin
if (test_objects_ready) then return;
call setup_test_objects();
end
procedure queue_full_sequence begin
reg_anim_begin();
reg_anim_turn_towards(test_move_critter_obj, expected_turn_tile, -1);
reg_anim_take_out(test_takeout_critter_obj, TEST_TAKE_OUT_FRAME, -1);
reg_anim_light(test_light_obj, TEST_LIGHT_VALUE, -1);
reg_anim_change_fid(test_fid_obj, expected_fid, -1);
reg_anim_animate_and_hide(test_hide_obj, TEST_STAND_ANIM, -1);
reg_anim_destroy(test_destroy_obj);
reg_anim_animate_and_move(test_move_critter_obj, expected_move_tile, TEST_WALK_ANIM, -1);
reg_anim_end();
end
procedure start_blocked_test begin
pending_test := 1;
pending_timeout_frames := 30;
call ensure_test_objects();
call queue_full_sequence();
display_msg("Started blocked-case test.");
end
procedure start_enabled_test begin
pending_test := 2;
pending_timeout_frames := 120;
call ensure_test_objects();
reg_anim_combat_check(0);
call queue_full_sequence();
display_msg("Started enabled-case test.");
end
procedure start_noncombat_test begin
pending_test := 3;
pending_timeout_frames := 120;
call ensure_test_objects();
call queue_full_sequence();
display_msg("Started non-combat opcode test.");
end
procedure keypress_handler begin
variable
pressed := get_sfall_arg_at(0),
key := get_sfall_arg_at(1);
if (not pressed) then return;
if (pending_test != 0) then begin
if (key == DIK_X or key == DIK_V or key == DIK_N) then
display_msg("A reg_anim_combat_check test is already running.");
return;
end
if (combat_data and key == DIK_X) then begin
display_msg("X: combat should block all reg_anim calls in this sequence.");
call start_blocked_test();
end else if (combat_data and key == DIK_V) then begin
display_msg("V: reg_anim_combat_check(0) should allow the registered animations in combat.");
call start_enabled_test();
end else if ((not combat_data) and key == DIK_N) then begin
if (noncombat_test_completed) then begin
display_msg("N already ran once. Reload the map or game before running it again.");
return;
end
display_msg("N: outside combat, the full reg_anim opcode sequence should run.");
call start_noncombat_test();
end else if (key == DIK_X or key == DIK_V) then begin
display_msg("Enter combat before running X or V.");
end else if (key == DIK_N) then begin
display_msg("Leave combat before running N.");
end
end
procedure report_blocked_results begin
variable errors_before := test_suite_errors;
call assertEquals("turn_towards blocked", obj_get_rot(test_move_critter_obj), initial_rotation);
call assertEquals("animate_and_move blocked", tile_num(test_move_critter_obj), initial_tile);
call assertEquals("change_fid blocked", get_object_data(test_fid_obj, OBJ_DATA_FID), initial_fid);
call assertEquals("animate_and_hide blocked", obj_is_visible_flag(test_hide_obj), initial_hide_visibility);
call assertEquals("destroy blocked", obj_is_visible_flag(test_destroy_obj), initial_destroy_visibility);
if (test_suite_errors == errors_before) then
display_msg("PASS blocked automatic checks");
else
display_msg("FAIL blocked automatic checks");
call report_test_results("reg_anim_combat_check_blocked");
end
procedure report_enabled_results begin
variable errors_before := test_suite_errors;
call assertNotEquals("turn_towards runs in combat when enabled", obj_get_rot(test_move_critter_obj), initial_rotation);
call assertEquals("animate_and_move runs in combat when enabled", tile_num(test_move_critter_obj), expected_move_tile);
call assertEquals("change_fid runs in combat when enabled", get_object_data(test_fid_obj, OBJ_DATA_FID), expected_fid);
if (test_suite_errors == errors_before) then
display_msg("PASS enabled automatic checks");
else
display_msg("FAIL enabled automatic checks");
call report_test_results("reg_anim_combat_check_enabled");
end
procedure report_noncombat_results begin
variable errors_before := test_suite_errors;
noncombat_test_completed := true;
call assertNotEquals("turn_towards outside combat", obj_get_rot(test_move_critter_obj), initial_rotation);
call assertEquals("animate_and_move outside combat", tile_num(test_move_critter_obj), expected_move_tile);
call assertEquals("change_fid outside combat", get_object_data(test_fid_obj, OBJ_DATA_FID), expected_fid);
call assertEquals("animate_and_hide outside combat", obj_is_visible_flag(test_hide_obj), 0);
if (test_suite_errors == errors_before) then
display_msg("PASS non-combat automatic checks");
else
display_msg("FAIL non-combat automatic checks");
call report_test_results("reg_anim_full_noncombat");
display_msg("Manual check: outside combat, the other clone should play take out, light should visibly apply if supported by the object type, and destroy_obj should be gone.");
end
procedure poll_pending_test begin
if (pending_test == 0) then return;
pending_frames += 1;
if (pending_frames < pending_timeout_frames) then return;
variable current_test := pending_test;
pending_test := 0;
pending_frames := 0;
pending_timeout_frames := 0;
if (current_test == 1) then begin
call report_blocked_results();
end else if (current_test == 2) then begin
call report_enabled_results();
end else if (current_test == 3) then begin
call report_noncombat_results();
end
end
procedure start begin
set_global_script_repeat(1);
if (not hook_registered and game_loaded) then begin
hook_registered := true;
call ensure_test_objects();
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
display_msg("reg_anim_combat_check test loaded. Enter combat for X/V, or stay out of combat for N (one-shot).");
end
call poll_pending_test();
end
+78
View File
@@ -0,0 +1,78 @@
#include "sfall.h"
#include "test_utils.h"
variable blocker_obj := 0;
procedure cleanup begin
if (blocker_obj) then begin
destroy_object(blocker_obj);
blocker_obj := 0;
end
end
procedure start begin
variable elev := elevation(dude_obj);
variable dude_tile := tile_num(dude_obj);
variable dir;
variable step_tile := -1;
variable step_dir := -1;
variable block_tile := -1;
variable path;
variable objs;
variable ground_fid;
variable roof_fid;
display_msg("Testing tile/path functions...");
call cleanup();
for (dir := 0; dir < 6; dir += 1) begin
variable candidate := tile_num_in_direction(dude_tile, dir, 1);
if (candidate != dude_tile and obj_blocking_tile(candidate, elev, BLOCKING_TYPE_BLOCK) == 0) then begin
if (step_tile == -1) then begin
step_tile := candidate;
step_dir := dir;
end
if (block_tile == -1 and candidate != step_tile) then begin
block_tile := candidate;
end
end
end
call assertNotEquals("found reachable neighboring tile", step_tile, -1);
if (step_tile != -1) then begin
path := path_find_to(dude_obj, step_tile, BLOCKING_TYPE_BLOCK);
call assertEquals("one-step path length", len_array(path), 1);
if (len_array(path) == 1) then
call assertEquals("one-step path direction", get_array(path, 0), step_dir);
end
if (block_tile != -1) then begin
blocker_obj := create_object_sid(obj_pid(dude_obj), block_tile, elev, -1);
call assertNotEquals("spawned blocking critter", blocker_obj, 0);
if (blocker_obj) then begin
path := path_find_to(dude_obj, tile_num(blocker_obj), BLOCKING_TYPE_BLOCK);
call assertEquals("occupied destination returns empty path", len_array(path), 0);
end
end else begin
display_msg("Skipping occupied-destination path check: no free adjacent tile for blocker.");
end
objs := tile_get_objs(dude_tile, elev);
call assertTrue("tile_get_objs contains dude", scan_array(objs, dude_obj) != -1);
ground_fid := get_tile_ground_fid(dude_tile, elev);
roof_fid := get_tile_roof_fid(dude_tile, elev);
call assertTrue("ground fid is non-negative", ground_fid >= 0);
call assertTrue("roof fid is non-negative", roof_fid >= 0);
call assertEquals("ground helper matches ext", ground_fid, get_tile_fid_ext(dude_tile, elev, 0));
call assertEquals("roof helper matches ext", roof_fid, get_tile_fid_ext(dude_tile, elev, 1));
call assertTrue("tile_light returns non-negative intensity", tile_light(elev, dude_tile) >= 0);
call assertEquals("tile_by_position metarule exists", metarule_exist("tile_by_position"), 1);
call assertEquals("tile_by_position matches tile_under_cursor", tile_by_position(get_mouse_x, get_mouse_y), tile_under_cursor);
tile_refresh_display;
call cleanup();
call report_test_results("tile_path");
end
+46
View File
@@ -3,6 +3,8 @@
#include "../sfall/define_lite.h"
#include "../test_utils.h"
#include "../sfall/lib.arrays.h"
variable last_game_mode;
variable gamemodechange_assertions_enabled := false;
variable gamemodechange_reported := false;
@@ -94,6 +96,47 @@ procedure gamemodechange_handler begin
last_game_mode := current_mode;
end
procedure combatdamage_handler begin
variable args := get_sfall_args;
display_msg(sprintf("combatdamage %s", debug_array_str(args)));
set_sfall_return(10); // target dmg
set_sfall_return(5);
set_sfall_return(0);
set_sfall_return(DAM_HIT); // attacker flag
set_sfall_return(10); // knockback
end
procedure barterprice_handler begin
variable
args := get_sfall_args,
value := args[2],
caps := args[4],
offerValue := args[7],
offerTable := args[6],
offerCaps := item_caps_total(offerTable);
display_msg(sprintf("barterprice %s", debug_array_str(args)));
set_sfall_return((value - caps) * 2 + caps);
set_sfall_return((offerValue - offerCaps) / 2 + offerCaps);
end
#define ANIM_fall_back (20)
#define ANIM_fall_front (21)
#define ANIM_fire_dance (33)
procedure deathanim_handler begin
variable
args := get_sfall_args;
display_msg(sprintf("deathanim %s", debug_array_str(args)));
set_sfall_return(ANIM_fire_dance);
end
procedure start begin
//call test_with_args(777);
display_msg("start hook test");
@@ -112,6 +155,9 @@ procedure start begin
register_hook_proc(HOOK_USEOBJ, useobj_handler);
register_hook_proc(HOOK_USEOBJON, useobjon_handler);
register_hook_proc(HOOK_GAMEMODECHANGE, gamemodechange_handler);
register_hook_proc(HOOK_COMBATDAMAGE, combatdamage_handler);
register_hook_proc(HOOK_BARTERPRICE, barterprice_handler);
register_hook_proc(HOOK_DEATHANIM2, deathanim_handler);
end
+1
View File
@@ -1,6 +1,7 @@
[ValidSection]
Key1=Value1
Key2=2
EmptyValue=
[SectionWithSpaces]
LeadingSpaceKey = LeadingSpaceValue
+1
View File
@@ -0,0 +1 @@
{100}{}{auto extra msg}
+81 -68
View File
@@ -29,6 +29,7 @@
#include "scripts.h"
#include "settings.h"
#include "sfall_config.h"
#include "sfall_script_hooks.h"
#include "skill.h"
#include "stat.h"
#include "text_object.h"
@@ -76,20 +77,20 @@ static const int gMaximumBloodDeathAnimations[DAMAGE_TYPE_COUNT] = {
// Note: some of these are callbacks that always take two Object*, but may not use them.
// Ignored parameters are marked with underscores.
static int actionKnockdown(Object* obj, int* anim, int maxDistance, int rotation, int delay);
static int _action_blood(Object* obj, int anim, int delay);
static int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront);
static int _check_death(Object* obj, int anim, int minViolenceLevel, bool hitFromFront);
static int actionBlood(Object* obj, int anim, int delay);
static int pickDeathAnim(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront);
static int checkDeathAnim(Object* obj, int anim, int minViolenceLevel, bool hitFromFront);
static int _internal_destroy(Object* _, Object* toDestroy);
static void _show_damage_to_object(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay);
static void showDamageToObject(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay);
static int _show_death(Object* obj, int anim);
static int _show_damage_extras(Attack* attack);
static void _show_damage(Attack* attack, int attackerAnimation, int delay);
static int showDamageToExtras(Attack* attack);
static void showDamage(Attack* attack, int attackerAnimation, int delay);
static int _action_melee(Attack* attack, int anim);
static int _action_ranged(Attack* attack, int anim);
static int _is_next_to(Object* obj1, Object* obj2);
static int _action_climb_ladder(Object* critter, Object* ladder);
static int _action_use_skill_in_combat_error(Object* critter);
static int _pick_fall(Object* obj, int anim);
static int pickFallAnim(Object* obj, int anim);
static int _report_explosion(Attack* attack, Object* sourceObj);
static int _finished_explosion(Object*, Object*);
static int _compute_explosion_damage(int min, int max, Object* defender, int* knockbackDistancePtr);
@@ -155,8 +156,10 @@ int actionKnockdown(Object* obj, int* anim, int maxDistance, int rotation, int d
return tile;
}
// 0x410568
int _action_blood(Object* obj, int anim, int delay)
// Plays bleeding animation (after critter has fallen with ANIM_FALL_BACK or ANIM_FALL_FRONT).
//
// 0x410568 action_blood
int actionBlood(Object* obj, int anim, int delay)
{
if (settings.preferences.violence_level == VIOLENCE_LEVEL_NONE) {
return anim;
@@ -181,9 +184,19 @@ int _action_blood(Object* obj, int anim, int delay)
return bloodyAnim;
}
// 0x41060C
int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront)
// 0x41060C pick_death
int pickDeathAnim(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront)
{
if (attacker->fid == buildFid(OBJ_TYPE_MISC, 10, 0, 0, 0)) { // roktxpd.frm
return checkDeathAnim(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
}
if (attacker->pid == PROTO_ID_FORCE_FIELD_NS) { // Forcefield North/South
return checkDeathAnim(defender, ANIM_ELECTRIFIED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
}
if (attacker->fid == FRAME_ID_FORCE_FIELD_NS) { // ffield03.frm
return checkDeathAnim(defender, attackerAnimation, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
}
int normalViolenceLevelDamageThreshold = 15;
int maximumBloodViolenceLevelDamageThreshold = 45;
@@ -209,7 +222,7 @@ int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage,
int violenceLevel = settings.preferences.violence_level;
if (critterFlagCheck(defender->pid, CRITTER_SPECIAL_DEATH)) {
return _check_death(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_NORMAL, hitFromFront);
return checkDeathAnim(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_NORMAL, hitFromFront);
}
bool hasBloodyMess = false;
@@ -241,7 +254,7 @@ int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage,
if (violenceLevel > VIOLENCE_LEVEL_MINIMAL && (hasBloodyMess || normalViolenceLevelDamageThreshold <= damage)) {
if (violenceLevel > VIOLENCE_LEVEL_NORMAL && (hasBloodyMess || maximumBloodViolenceLevelDamageThreshold <= damage)) {
deathAnim = gMaximumBloodDeathAnimations[damageType];
if (_check_death(defender, deathAnim, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront) != deathAnim) {
if (checkDeathAnim(defender, deathAnim, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront) != deathAnim) {
deathAnim = gNormalDeathAnimations[damageType];
}
} else {
@@ -255,11 +268,14 @@ int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage,
deathAnim = ANIM_FALL_FRONT;
}
return _check_death(defender, deathAnim, VIOLENCE_LEVEL_NONE, hitFromFront);
return checkDeathAnim(defender, deathAnim, VIOLENCE_LEVEL_NONE, hitFromFront);
}
// 0x410814
int _check_death(Object* obj, int anim, int minViolenceLevel, bool hitFromFront)
// Returns anim if art for it exists and selected violence_level >= minViolenceLevel.
// Otherwise, returns neutral fall animation (front or back, based on hitFromFront).
//
// 0x410814 check_death
int checkDeathAnim(Object* obj, int anim, int minViolenceLevel, bool hitFromFront)
{
int fid;
@@ -275,7 +291,8 @@ int _check_death(Object* obj, int anim, int minViolenceLevel, bool hitFromFront)
}
fid = buildFid(OBJ_TYPE_CRITTER, obj->fid & 0xFFF, ANIM_FALL_FRONT, (obj->fid & 0xF000) >> 12, obj->rotation + 1);
if (artExists(fid)) {
// CE: fixed vanilla logic that returned ANIM_FALL_BACK if artExists for ANIM_FALL_FRONT returned true.
if (!artExists(fid)) {
return ANIM_FALL_BACK;
}
@@ -290,8 +307,8 @@ int _internal_destroy(Object* _, Object* toDestroy)
// TODO: Check very carefully, lots of conditions and jumps.
//
// 0x4108D0
void _show_damage_to_object(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay)
// 0x4108D0 show_damage_to_object
void showDamageToObject(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay)
{
int anim;
int fid;
@@ -304,30 +321,22 @@ void _show_damage_to_object(Object* defender, int damage, int flags, Object* wea
anim = FID_ANIM_TYPE(defender->fid);
if (!critterIsProne(defender)) {
if ((flags & DAM_DEAD) != 0) {
fid = buildFid(OBJ_TYPE_MISC, 10, 0, 0, 0);
if (fid == attacker->fid) {
anim = _check_death(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
} else if (attacker->pid == PROTO_ID_0x20001EB) {
anim = _check_death(defender, ANIM_ELECTRIFIED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
} else if (attacker->fid == FID_0x20001F5) {
anim = _check_death(defender, attackerAnimation, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
} else {
anim = _pick_death(attacker, defender, weapon, damage, attackerAnimation, hitFromFront);
}
anim = pickDeathAnim(attacker, defender, weapon, damage, attackerAnimation, hitFromFront);
scriptHooks_DeathAnim(attacker, defender, weapon, damage, &anim);
if (anim != ANIM_FIRE_DANCE) {
if (knockbackDistance != 0 && (anim == ANIM_FALL_FRONT || anim == ANIM_FALL_BACK)) {
actionKnockdown(defender, &anim, knockbackDistance, knockbackRotation, delay);
anim = _action_blood(defender, anim, -1);
anim = actionBlood(defender, anim, -1);
} else {
sfx_name = sfxBuildCharName(defender, anim, CHARACTER_SOUND_EFFECT_DIE);
animationRegisterPlaySoundEffect(defender, sfx_name, delay);
anim = _pick_fall(defender, anim);
anim = pickFallAnim(defender, anim);
animationRegisterAnimate(defender, anim, 0);
if (anim == ANIM_FALL_FRONT || anim == ANIM_FALL_BACK) {
anim = _action_blood(defender, anim, -1);
anim = actionBlood(defender, anim, -1);
}
}
} else {
@@ -406,7 +415,7 @@ void _show_damage_to_object(Object* defender, int damage, int flags, Object* wea
if (knockbackDistance != 0) {
actionKnockdown(defender, &anim, knockbackDistance, knockbackRotation, 0);
} else {
anim = _pick_fall(defender, anim);
anim = pickFallAnim(defender, anim);
animationRegisterAnimate(defender, anim, 0);
}
} else if ((flags & DAM_ON_FIRE) != 0 && artExists(buildFid(OBJ_TYPE_CRITTER, defender->fid & 0xFFF, ANIM_FIRE_DANCE, (defender->fid & 0xF000) >> 12, defender->rotation + 1))) {
@@ -439,7 +448,7 @@ void _show_damage_to_object(Object* defender, int damage, int flags, Object* wea
}
} else {
if ((flags & DAM_DEAD) != 0 && (defender->data.critter.combat.results & DAM_DEAD) == 0) {
anim = _action_blood(defender, anim, delay);
anim = actionBlood(defender, anim, delay);
} else {
return;
}
@@ -508,8 +517,10 @@ int _show_death(Object* obj, int anim)
return 0;
}
// 0x410FEC
int _show_damage_extras(Attack* attack)
// Animates damage to extras in a given attack (secondary targets).
//
// 0x410FEC _show_damage_extras
int showDamageToExtras(Attack* attack)
{
for (int index = 0; index < attack->extrasLength; index++) {
Object* obj = attack->extras[index];
@@ -520,7 +531,7 @@ int _show_damage_extras(Attack* attack)
_register_priority(1);
int attackerAnimation = critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, obj->tile);
_show_damage_to_object(obj, attack->extrasDamage[index], attack->extrasFlags[index], attack->weapon, hitFromFront, attack->extrasKnockback[index], knockbackRotation, attackerAnimation, attack->attacker, 0);
showDamageToObject(obj, attack->extrasDamage[index], attack->extrasFlags[index], attack->weapon, hitFromFront, attack->extrasKnockback[index], knockbackRotation, attackerAnimation, attack->attacker, 0);
reg_anim_end();
}
}
@@ -528,8 +539,16 @@ int _show_damage_extras(Attack* attack)
return 0;
}
// 0x4110AC
void _show_damage(Attack* attack, int attackerAnimation, int delay)
// Shows animation to attacker, in case of critical failure, backwash, etc.
static void showDamageToAttacker(Attack* attack, int attackerAnimation)
{
showDamageToObject(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, true, 0, 0, attackerAnimation, attack->attacker, -1);
}
// Animates damage to defender (or attacker, in some cases) as a result of a given attack.
//
// 0x4110AC show_damage
void showDamage(Attack* attack, int attackerAnimation, int delay)
{
for (int index = 0; index < attack->extrasLength; index++) {
Object* object = attack->extras[index];
@@ -540,33 +559,26 @@ void _show_damage(Attack* attack, int attackerAnimation, int delay)
}
if ((attack->attackerFlags & DAM_HIT) == 0) {
if ((attack->attackerFlags & DAM_CRITICAL) != 0) {
_show_damage_to_object(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, 1, 0, 0, attackerAnimation, attack->attacker, -1);
} else if ((attack->attackerFlags & DAM_BACKWASH) != 0) {
_show_damage_to_object(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, 1, 0, 0, attackerAnimation, attack->attacker, -1);
if ((attack->attackerFlags & DAM_CRITICAL) != 0 || (attack->attackerFlags & DAM_BACKWASH) != 0) {
showDamageToAttacker(attack, attackerAnimation);
}
} else {
if (attack->defender != nullptr) {
// NOTE: Uninline.
bool hitFromFront = _is_hit_from_front(attack->defender, attack->attacker);
bool hitFromFront = _is_hit_from_front(attack->attacker, attack->defender);
if (FID_TYPE(attack->defender->fid) == OBJ_TYPE_CRITTER) {
if (attack->attacker->fid == FID_0x20001F5) {
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
_show_damage_to_object(attack->defender, attack->defenderDamage, attack->defenderFlags, attack->weapon, hitFromFront, attack->defenderKnockback, knockbackRotation, attackerAnimation, attack->attacker, delay);
} else {
int weaponAnimation = critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
_show_damage_to_object(attack->defender, attack->defenderDamage, attack->defenderFlags, attack->weapon, hitFromFront, attack->defenderKnockback, knockbackRotation, weaponAnimation, attack->attacker, delay);
}
} else {
tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
int attackerAnimForShow = attack->attacker->fid == FRAME_ID_FORCE_FIELD_NS
? attackerAnimation
: critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
showDamageToObject(attack->defender, attack->defenderDamage, attack->defenderFlags, attack->weapon, hitFromFront, attack->defenderKnockback, knockbackRotation, attackerAnimForShow, attack->attacker, delay);
}
}
if ((attack->attackerFlags & DAM_DUD) != 0) {
_show_damage_to_object(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, 1, 0, 0, attackerAnimation, attack->attacker, -1);
showDamageToAttacker(attack, attackerAnimation);
}
}
}
@@ -643,7 +655,7 @@ int _action_melee(Attack* attack, int anim)
animationRegisterAnimate(attack->attacker, anim, 0);
animationRegisterPlaySoundEffect(attack->attacker, sfx_name_temp, delay);
_show_damage(attack, anim, 0);
showDamage(attack, anim, 0);
} else {
if (attack->defender->data.critter.combat.results & 0x03) {
animationRegisterPlaySoundEffect(attack->attacker, sfx_name_temp, -1);
@@ -685,7 +697,7 @@ int _action_melee(Attack* attack, int anim)
return -1;
}
_show_damage_extras(attack);
showDamageToExtras(attack);
return 0;
}
@@ -914,7 +926,7 @@ int _action_ranged(Attack* attack, int anim)
}
}
_show_damage(attack, anim, delay);
showDamage(attack, anim, delay);
if ((attack->attackerFlags & DAM_HIT) == 0) {
_combatai_msg(attack->defender, attack, AI_MESSAGE_TYPE_MISS, -1);
@@ -981,7 +993,7 @@ int _action_ranged(Attack* attack, int anim)
return -1;
}
_show_damage_extras(attack);
showDamageToExtras(attack);
return 0;
}
@@ -1511,7 +1523,7 @@ int actionUseSkill(Object* user, Object* target, int skill)
}
// 0x412BC4
bool _is_hit_from_front(Object* attacker, Object* defender)
bool _is_hit_from_front(const Object* attacker, const Object* defender)
{
int diff = attacker->rotation - defender->rotation;
if (diff < 0) {
@@ -1534,9 +1546,10 @@ bool _can_see(Object* source, Object* target)
return diff == 0 || diff == 1 || diff == 5;
}
// looks like it tries to change fall animation depending on object's current rotation
// 0x412C1C
int _pick_fall(Object* obj, int anim)
// Tries to change between ANIM_FALL_FRONT and ANIM_FALL_BACK based on available free space in front or behind and presence of art frames.
//
// 0x412C1C pick_fall
int pickFallAnim(Object* obj, int anim)
{
int i;
int rotation;
@@ -1666,7 +1679,7 @@ int actionExplode(int tile, int elevation, int minDamage, int maxDamage, Object*
animationRegisterPlaySoundEffect(explosion, "whn1xxx1", 0);
animationRegisterUnsetFlag(explosion, OBJECT_HIDDEN, 0);
animationRegisterAnimateAndHide(explosion, ANIM_STAND, 0);
_show_damage(attack, 0, 1);
showDamage(attack, 0, 1);
for (int rotation = 0; rotation < ROTATION_COUNT; rotation++) {
animationRegisterUnsetFlag(adjacentExplosions[rotation], OBJECT_HIDDEN, 0);
@@ -1697,7 +1710,7 @@ int actionExplode(int tile, int elevation, int minDamage, int maxDamage, Object*
return -1;
}
_show_damage_extras(attack);
showDamageToExtras(attack);
} else {
if (critter != nullptr) {
if ((attack->defenderFlags & DAM_DEAD) != 0) {
@@ -1897,7 +1910,7 @@ void actionDamage(int tile, int elevation, int minDamage, int maxDamage, int dam
}
Object* attacker;
if (objectCreateWithFidPid(&attacker, FID_0x20001F5, -1) == -1) {
if (objectCreateWithFidPid(&attacker, FRAME_ID_FORCE_FIELD_NS, -1) == -1) {
internal_free(attack);
return;
}
@@ -1932,7 +1945,7 @@ void actionDamage(int tile, int elevation, int minDamage, int maxDamage, int dam
if (animated) {
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
animationRegisterPlaySoundEffect(attacker, "whc1xxx1", 0);
_show_damage(attack, gMaximumBloodDeathAnimations[damageType], 0);
showDamage(attack, gMaximumBloodDeathAnimations[damageType], 0);
animationRegisterCallbackForced(attack, nullptr, (AnimationCallback*)_report_dmg, 0);
animationRegisterHideObjectForced(attacker);
+1 -1
View File
@@ -15,7 +15,7 @@ int actionPickUp(Object* critter, Object* item);
int _action_loot_container(Object* critter, Object* container);
int _action_skill_use(int skill);
int actionUseSkill(Object* user, Object* target, int skill);
bool _is_hit_from_front(Object* attacker, Object* defender);
bool _is_hit_from_front(const Object* attacker, const Object* defender);
bool _can_see(Object* source, Object* target);
bool _action_explode_running();
int actionExplode(int tile, int elevation, int minDamage, int maxDamage, Object* sourceObj, bool animate);
+20 -1
View File
@@ -226,6 +226,24 @@ typedef struct AnimationSequence {
AnimationDescription animations[ANIMATION_DESCRIPTION_LIST_CAPACITY];
} AnimationSequence;
// sfall: "in combat" check for animations is overridable
static bool gRegAnimCombatCheck = true;
bool animationCheckCombatMode()
{
return gRegAnimCombatCheck && isInCombat();
}
void animationSetCombatCheck(bool enable)
{
gRegAnimCombatCheck = enable;
}
void animationResetCombatCheck()
{
gRegAnimCombatCheck = true;
}
typedef struct PathNode {
int tile;
int from;
@@ -937,6 +955,7 @@ int animationRegisterAnimateAndHide(Object* owner, int anim, int delay)
}
// 0x414C50
// TODO: original function likely handled both tiles and object
int animationRegisterRotateToTile(Object* owner, int tile)
{
if (_check_registry(owner) == -1) {
@@ -1206,7 +1225,7 @@ int animationRegisterTakeOutWeapon(Object* owner, int weaponAnimationCode, int d
AnimationDescription* animationDescription = &(animationSequence->animations[gAnimationDescriptionCurrentIndex]);
animationDescription->kind = ANIM_KIND_TAKE_OUT_WEAPON;
animationDescription->anim = ANIM_TAKE_OUT;
animationDescription->delay = 0;
animationDescription->delay = 0; // TODO: should use `delay`?
animationDescription->owner = owner;
animationDescription->weaponAnimationCode = weaponAnimationCode;
+3
View File
@@ -112,6 +112,9 @@ typedef Object* PathBuilderCallback(Object* object, int tile, int elevation);
void animationInit();
void animationReset();
void animationExit();
bool animationCheckCombatMode();
void animationSetCombatCheck(bool enable);
void animationResetCombatCheck();
int reg_anim_begin(int a1);
int _register_priority(int a1);
int reg_anim_clear(Object* a1);
+26 -22
View File
@@ -88,8 +88,8 @@ typedef struct DamageCalculationContext {
int damageResistance;
int damageThreshold;
int damageBonus;
int bonusDamageMultiplier;
int combatDifficultyDamageModifier;
int baseDamageMult;
int difficultyDamagePercent;
} DamageCalculationContext;
static bool _combat_safety_invalidate_weapon_func(Object* attacker, Object* weapon, int hitMode, Object* defender, int* safeDistancePtr, Object* attackerFriend);
@@ -119,7 +119,7 @@ static int _attackFindInvalidFlags(Object* a1, Object* a2);
static int attackComputeCriticalFailure(Attack* attack);
static void _do_random_cripple(int* flagsPtr);
static int attackDetermineToHit(Object* attacker, int tile, Object* defender, int hitLocation, int hitMode, bool useDistance);
static void attackComputeDamage(Attack* attack, int ammoQuantity, int a3);
static void attackComputeDamage(Attack* attack, int numRounds, int baseDamageMult);
static void _check_for_death(Object* a1, int a2, int* a3);
static void _set_new_results(Object* a1, int a2);
static void _damage_object(Object* a1, int damage, bool animated, int a4, Object* a5);
@@ -4232,8 +4232,8 @@ static int attackComputeCriticalFailure(Attack* attack)
attack->attackerFlags &= ~v17;
if ((attack->attackerFlags & DAM_HIT_SELF) != 0) {
int ammoQuantity = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
attackComputeDamage(attack, ammoQuantity, 2);
int rounds = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
attackComputeDamage(attack, rounds, 2);
} else if ((attack->attackerFlags & DAM_EXPLODE) != 0) {
attackComputeDamage(attack, 1, 2);
}
@@ -4262,8 +4262,8 @@ static int attackComputeCriticalFailure(Attack* attack)
attack->defenderHitLocation = HIT_LOCATION_TORSO;
attack->attackerFlags &= ~DAM_CRITICAL;
int ammoQuantity = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
attackComputeDamage(attack, ammoQuantity, 2);
int rounds = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
attackComputeDamage(attack, rounds, 2);
} else {
attack->defender = attack->oops;
}
@@ -4506,7 +4506,7 @@ static int attackDetermineToHit(Object* attacker, int tile, Object* defender, in
}
// 0x4247B8
static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamageMultiplier)
static void attackComputeDamage(Attack* attack, int numRounds, int baseDamageMult)
{
int* damagePtr;
Object* critter;
@@ -4528,6 +4528,8 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
*damagePtr = 0;
if (FID_TYPE(critter->fid) != OBJ_TYPE_CRITTER) {
// This is to match sfall behavior as it wraps attackComputeDamage call and always invokes hook, even in this case.
scriptHooks_ComputeDamage(attack, numRounds, baseDamageMult);
return;
}
@@ -4557,14 +4559,14 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
damageBonus = 0;
}
int combatDifficultyDamageModifier = 100;
int difficultyDamagePercent = 100;
if (attack->attacker->data.critter.combat.team != gDude->data.critter.combat.team) {
switch (settings.preferences.combat_difficulty) {
case COMBAT_DIFFICULTY_EASY:
combatDifficultyDamageModifier = 75;
difficultyDamagePercent = 75;
break;
case COMBAT_DIFFICULTY_HARD:
combatDifficultyDamageModifier = 125;
difficultyDamagePercent = 125;
break;
}
}
@@ -4576,8 +4578,8 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
context.damageResistance = damageResistance;
context.damageThreshold = damageThreshold;
context.damageBonus = damageBonus;
context.bonusDamageMultiplier = bonusDamageMultiplier;
context.combatDifficultyDamageModifier = combatDifficultyDamageModifier;
context.baseDamageMult = baseDamageMult;
context.difficultyDamagePercent = difficultyDamagePercent;
if (gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ || gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ_WITH_DAMAGE_MULTIPLIER_TWEAK) {
damageModCalculateGlovz(&context);
@@ -4591,10 +4593,10 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
damageResistance = 0;
}
int damageMultiplier = bonusDamageMultiplier * weaponGetAmmoDamageMultiplier(attack->weapon);
int damageMultiplier = baseDamageMult * weaponGetAmmoDamageMultiplier(attack->weapon);
int damageDivisor = weaponGetAmmoDamageDivisor(attack->weapon);
for (int index = 0; index < ammoQuantity; index++) {
for (int index = 0; index < numRounds; index++) {
int damage = weaponGetDamage(attack->attacker, attack->hitMode);
damage += damageBonus;
@@ -4608,7 +4610,7 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
// TODO: Why we're halving it?
damage /= 2;
damage *= combatDifficultyDamageModifier;
damage *= difficultyDamagePercent;
damage /= 100;
damage -= damageThreshold;
@@ -4665,6 +4667,8 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
}
}
}
scriptHooks_ComputeDamage(attack, numRounds, baseDamageMult);
}
// 0x424BAC
@@ -6692,9 +6696,9 @@ static void damageModCalculateGlovz(DamageCalculationContext* context)
int calculatedDamageResistance = context->damageResistance;
if (calculatedDamageResistance > 0) {
if (context->combatDifficultyDamageModifier > 100) {
if (context->difficultyDamagePercent > 100) {
calculatedDamageResistance -= 20;
} else if (context->combatDifficultyDamageModifier < 100) {
} else if (context->difficultyDamagePercent < 100) {
calculatedDamageResistance += 20;
}
@@ -6740,9 +6744,9 @@ static void damageModCalculateGlovz(DamageCalculationContext* context)
}
if (gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ_WITH_DAMAGE_MULTIPLIER_TWEAK) {
damage += damageModGlovzDivRound(damage * context->bonusDamageMultiplier * 25, 100);
damage += damageModGlovzDivRound(damage * context->baseDamageMult * 25, 100);
} else {
damage += damage * context->bonusDamageMultiplier / 2;
damage += damage * context->baseDamageMult / 2;
}
if (damage > 0) {
@@ -6775,7 +6779,7 @@ static int damageModGlovzDivRound(int dividend, int divisor)
static void damageModCalculateYaam(DamageCalculationContext* context)
{
int damageMultiplier = context->bonusDamageMultiplier * weaponGetAmmoDamageMultiplier(context->attack->weapon);
int damageMultiplier = context->baseDamageMult * weaponGetAmmoDamageMultiplier(context->attack->weapon);
int damageDivisor = weaponGetAmmoDamageDivisor(context->attack->weapon);
int ammoDamageResistance = weaponGetAmmoDamageResistanceModifier(context->attack->weapon);
@@ -6812,7 +6816,7 @@ static void damageModCalculateYaam(DamageCalculationContext* context)
}
damage /= 2;
damage *= context->combatDifficultyDamageModifier;
damage *= context->difficultyDamagePercent;
damage /= 100;
damage -= damage * damageResistance / 100;
+1 -1
View File
@@ -641,7 +641,7 @@ int fileNameListInit(const char* pattern, char*** fileNameListPtr)
char dir[COMPAT_MAX_DIR];
char fileName[COMPAT_MAX_FNAME];
char extension[COMPAT_MAX_EXT];
compat_windows_path_to_native(name);
compat_path_to_native(name);
compat_splitpath(name, nullptr, dir, fileName, extension);
if (!isWildcard || *dir == '\0' || (strchr(dir, '\\') == nullptr && strchr(dir, '/') == nullptr)) {
+26 -4
View File
@@ -42,6 +42,7 @@ static DFile* dfileOpenInternal(DBase* dbase, const char* filename, const char*
static int dfileReadCharInternal(DFile* stream);
static bool dfileReadCompressed(DFile* stream, void* ptr, size_t size);
static void dfileUngetCompressed(DFile* stream, int ch);
static bool dfilePathMatchesPattern(const char* pattern, const char* path);
// Reads .DAT file contents.
//
@@ -201,11 +202,15 @@ bool dbaseClose(DBase* dbase)
// 0x4E5308
bool dbaseFindFirstEntry(DBase* dbase, DFileFindData* findFileData, const char* pattern)
{
// fpattern-matching requires native-separators
assert(pattern != nullptr);
compat_strlcpy(findFileData->pattern, pattern, sizeof(findFileData->pattern));
compat_path_to_native(findFileData->pattern);
for (int index = 0; index < dbase->entriesLength; index++) {
DBaseEntry* entry = &(dbase->entries[index]);
if (fpattern_match(pattern, entry->path)) {
if (dfilePathMatchesPattern(findFileData->pattern, entry->path)) {
strcpy(findFileData->fileName, entry->path);
strcpy(findFileData->pattern, pattern);
findFileData->index = index;
return true;
}
@@ -219,7 +224,7 @@ bool dbaseFindNextEntry(DBase* dbase, DFileFindData* findFileData)
{
for (int index = findFileData->index + 1; index < dbase->entriesLength; index++) {
DBaseEntry* entry = &(dbase->entries[index]);
if (fpattern_match(findFileData->pattern, entry->path)) {
if (dfilePathMatchesPattern(findFileData->pattern, entry->path)) {
strcpy(findFileData->fileName, entry->path);
findFileData->index = index;
return true;
@@ -638,7 +643,15 @@ static int dbaseFindEntryByFilePath(const void* file, const void* entryName)
// 0x4E5D9C
static DFile* dfileOpenInternal(DBase* dbase, const char* filePath, const char* mode, DFile* dfile)
{
DBaseEntry* entry = (DBaseEntry*)bsearch(filePath, dbase->entries, dbase->entriesLength, sizeof(*dbase->entries), dbaseFindEntryByFilePath);
assert(filePath != nullptr);
// .dat files contain windows path separators
char normalizedFilePath[COMPAT_MAX_PATH];
compat_strlcpy(normalizedFilePath, filePath, sizeof(normalizedFilePath));
compat_path_to_windows(normalizedFilePath);
DBaseEntry* entry;
entry = (DBaseEntry*)bsearch(normalizedFilePath, dbase->entries, dbase->entriesLength, sizeof(*dbase->entries), dbaseFindEntryByFilePath);
if (entry == nullptr) {
goto err;
}
@@ -860,4 +873,13 @@ static void dfileUngetCompressed(DFile* stream, int ch)
stream->position--;
}
// pattern must be normalized to native paths, since that's what fpattern requires
static bool dfilePathMatchesPattern(const char* pattern, const char* path)
{
char normalizedPath[COMPAT_MAX_PATH];
compat_strlcpy(normalizedPath, path, sizeof(normalizedPath));
compat_path_to_native(normalizedPath);
return fpattern_match(pattern, normalizedPath);
}
} // namespace fallout
+11 -7
View File
@@ -3,6 +3,7 @@
#include <string.h>
#include <fstream>
#include <string>
#include "art.h"
#include "color.h"
@@ -229,9 +230,9 @@ void displayMonitorExit()
}
// 0x43186C
void displayMonitorAddMessage(char* str)
void displayMonitorAddMessage(const char* str)
{
if (!gDisplayMonitorInitialized) {
if (!gDisplayMonitorInitialized || str == nullptr) {
return;
}
@@ -256,10 +257,13 @@ void displayMonitorAddMessage(char* str)
}
}
std::string mutableMessage(str);
char* mutableStr = mutableMessage.data();
// TODO: Refactor these two loops.
char* splitPos = nullptr;
while (true) {
while (fontGetStringWidth(str) < DISPLAY_MONITOR_WIDTH - _max_disp - knobWidth) {
while (fontGetStringWidth(mutableStr) < DISPLAY_MONITOR_WIDTH - _max_disp - knobWidth) {
char* temp = gDisplayMonitorLines[_disp_start];
int length;
if (knob != '\0') {
@@ -270,7 +274,7 @@ void displayMonitorAddMessage(char* str)
} else {
length = DISPLAY_MONITOR_LINE_LENGTH - 1;
}
strncpy(temp, str, length);
strncpy(temp, mutableStr, length);
gDisplayMonitorLines[_disp_start][DISPLAY_MONITOR_LINE_LENGTH - 1] = '\0';
_disp_start = (_disp_start + 1) % gDisplayMonitorLinesCapacity;
@@ -281,12 +285,12 @@ void displayMonitorAddMessage(char* str)
return;
}
str = splitPos + 1;
mutableStr = splitPos + 1;
*splitPos = ' ';
splitPos = nullptr;
}
char* space = strrchr(str, ' ');
char* space = strrchr(mutableStr, ' ');
if (space == nullptr) {
break;
}
@@ -311,7 +315,7 @@ void displayMonitorAddMessage(char* str)
} else {
length = DISPLAY_MONITOR_LINE_LENGTH - 1;
}
strncpy(temp, str, length);
strncpy(temp, mutableStr, length);
gDisplayMonitorLines[_disp_start][DISPLAY_MONITOR_LINE_LENGTH - 1] = '\0';
_disp_start = (_disp_start + 1) % gDisplayMonitorLinesCapacity;
+1 -1
View File
@@ -6,7 +6,7 @@ namespace fallout {
int displayMonitorInit();
int displayMonitorReset();
void displayMonitorExit();
void displayMonitorAddMessage(char* string);
void displayMonitorAddMessage(const char* string);
void displayMonitorDisable();
void displayMonitorEnable();
+19 -14
View File
@@ -128,6 +128,23 @@ int gSplashScreenScaling = 0;
// CE: Sonora folks like to store objects in global variables.
static void** gGameGlobalPointers = nullptr;
static void debugModeInit()
{
// CE: Handle debug mode (exactly as seen in `mapper2.exe`).
const char* debugMode = settings.debug.mode.c_str();
if (compat_stricmp(debugMode, "environment") == 0) {
_debug_register_env();
} else if (compat_stricmp(debugMode, "screen") == 0) {
_debug_register_screen();
} else if (compat_stricmp(debugMode, "log") == 0) {
_debug_register_log("debug.log", "wt");
} else if (compat_stricmp(debugMode, "mono") == 0) {
_debug_register_mono();
} else if (compat_stricmp(debugMode, "gnw") == 0) {
_debug_register_func(_win_debug);
}
}
// 0x442580
int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int flags, int argc, char** argv)
{
@@ -146,6 +163,8 @@ int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int fl
settingsInit(isMapper, argc, argv);
debugModeInit();
gIsMapper = isMapper;
if (gameDbInit() == -1) {
@@ -192,20 +211,6 @@ int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int fl
showSplash();
}
// CE: Handle debug mode (exactly as seen in `mapper2.exe`).
const char* debugMode = settings.debug.mode.c_str();
if (compat_stricmp(debugMode, "environment") == 0) {
_debug_register_env();
} else if (compat_stricmp(debugMode, "screen") == 0) {
_debug_register_screen();
} else if (compat_stricmp(debugMode, "log") == 0) {
_debug_register_log("debug.log", "wt");
} else if (compat_stricmp(debugMode, "mono") == 0) {
_debug_register_mono();
} else if (compat_stricmp(debugMode, "gnw") == 0) {
_debug_register_func(_win_debug);
}
interfaceFontsInit();
fontManagerAdd(&gModernFontManager);
fontSetCurrent(font);

Some files were not shown because too many files have changed in this diff Show More