Compare commits

...
Author SHA1 Message Date
github-actions[bot] 07939225c6 chore: auto-format with clang-format 2026-06-23 05:27:36 +00:00
Mike Klaas b10526330c SFALL_COMPAT 2026-06-22 22:26:13 -07:00
Mike Klaas 4c1cad7122 scriptWindow opcodes 2026-06-22 22:26:02 -07:00
Mike Klaas b76ff8257e fix item_weight 2026-06-22 22:24:46 -07:00
Mike Klaas 6edbd70d60 Let FrmImage reference single-frames from multi-frame frms, and support pcx 2026-06-22 22:04:33 -07:00
Mike Klaas 15917dccb3 inventory_refresh, display_stats 2026-06-22 20:56:19 -07:00
Mike Klaas 33aae49066 character editor display_stats 2026-06-22 20:46:55 -07:00
Mike Klaas bfd41bb40c opcode registration 2026-06-22 20:44:36 -07:00
Mike Klaas 0ccaef5ef0 get windows 2026-06-22 20:34:11 -07:00
Mike Klaas 26d7b632fc HOOK_MOVECOST (#496) 2026-06-17 06:11:08 +00:00
c6818f0849 Fix ddraw.ini -> game.cfg migration on unix-y systems (#497)
* Fix ddraw.ini -> game.cfg migration on unix-y systems

We were using `compat_splitpath` on a windows path.  We want to work on windows paths, so that's good.  This fixes compat_splitpath so that it works with both folder separators.  Also tweak `compat_makepath` to recognize both, for consistency.

This means that filenames with `\` will be broken, but I think we can (and should) live with that)

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* chore: auto-format with clang-format

* fix

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-16 22:48:46 -07:00
Mike Klaas 84cac43da1 HOOK_ITEMDAMAGE (#494)
* HOOK_ITEMDAMAGE
2026-06-16 22:22:53 -07:00
Mike Klaas 261b54c603 Implement unwield_slot (#488)
* Implement `unwield_slot`

This one was surprisingly subtle.  Supports many game modes and handles things differently depending on game mode.
2026-06-08 22:25:46 -07:00
Vlad K 46b89abbd8 Merge pull request #490 from fallout2-ce/feature/mapper-edg
Mapper EDG support
2026-06-08 23:17:25 +02:00
phobos2077 1ee64988bc Guard against saving EDG when saving SAV maps in gameplay 2026-06-07 20:53:35 +02:00
Vlad KandCopilot Autofix powered by AI c78ba885d7 Guard against using dev_path out of mapper
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-07 20:44:34 +02:00
phobos2077 6835d10590 mapper: fix TMP map files not being deleted, fix edg not loading when changing maps in play mode 2026-06-07 02:05:57 +02:00
phobos2077 20ad8d8778 mapper: fix SAV files being created in the dev_path 2026-06-07 01:17:01 +02:00
Vlad K 8c98a15ef6 sfall: get_ini_config + config cache (#489)
* sfall: get_ini_config + config cache

* Code review fixes and comment standardization
2026-06-06 22:34:37 +00:00
github-actions[bot] 6782a21185 chore: auto-format with clang-format 2026-06-06 21:33:20 +00:00
phobos2077 d12dbca951 mapper: EDG angled edges (squareRect) editing 2026-06-06 23:27:51 +02:00
phobos2077 823858f275 mapper: fix selected object being deleted when opening map 2026-06-06 23:27:50 +02:00
phobos2077 b93591f5d7 EDG: fix incorrect image corruption when scroll limiting kicks in right at the edge border 2026-06-06 23:27:50 +02:00
phobos2077 a91e40e3cc mapper: EDG support (edges only) 2026-06-06 23:27:50 +02:00
phobos2077 7da7d75f30 mapper: move common utility functions to mp_utils.cc 2026-06-06 23:27:50 +02:00
phobos2077 72b4680ff5 mapper: implement dev_path, a path override where saved files will go 2026-06-06 23:27:50 +02:00
Mike Klaas 99a762b649 frm2png can convert to/from indexed pngs (#487)
* frm2png can convert to/from indexed pngs
2026-06-05 20:39:56 -07:00
Mike KlaasandCopilot Autofix powered by AI 1afe925cbf Better fix for directory detection (#486)
* Better fix for directory detection

A couple places we used compat_file_exists to distinguish between files and folders/dirs, but this breaks on POSIX.  This attempts to fix it more holistically

* Make compat_file_exists to be consistent

All platforms now return false for folders/dirs

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 20:24:59 -07:00
Mike Klaas 95c07dba9f Fix compile warnings (#482)
Most are of this type

```
warning: 'size' overrides a member function but is not marked 'override' [-Winconsistent-missing-override]

sfall_arrays.cc:313:26: note: overridden virtual function is here
    313 |     virtual ProgramValue GetArrayKey(int index, Program* program) = 0;
        |
```
2026-06-04 08:27:48 -07:00
Mike Klaas 0fc042ef84 Fix save games being missing on Mac OS (#484) 2026-06-03 22:46:57 -07:00
Mike Klaas 29df63dd99 [HRP] Scale movies (#478)
SDL Surface for performance and nice scaling
2026-06-03 21:16:16 -07:00
Mike Klaas 6f2693edf7 Change use_walk_distance to match sfall setting (#481)
* Change `use_walk_distance` to match sfall setting

It's pointless to set this to 0,1 because you don't move if the target is adjacent or yourself, so sfall changes this to "number of spaces" between player and target.  We were reading the old ddraw.ini setting before, incorrectly.

Also add a small optimization to not compute path for adjacent targets

* add comment

* return true when distance is self/adjacent
2026-06-03 15:34:55 -07:00
Mike Klaas e51004a3d1 Ability to use indexed pngs as interface art (#454)
* Ability to load indexed pngs in game

* Switch 2-col inventory .frms to .png
2026-06-02 23:48:08 -07:00
Vlad K 9aa2ae4bbc ZIP DB archive support (#480)
* ZIP: inital support (directly in DBase)

* ZIP: log and bail on any unsupported feature

* dfile.cc compile error fix

* ZIP: minor refactoring

* ZIP: code review fixes and refactoring

* ZIP: simpler and more robust code

* Formatting

* ZIP: remove unnecessary dataOffset normalization
2026-06-02 18:30:20 +02:00
Vlad K 968534314d gameDbInit: change load order to keep master_patches at the top and to match sfall (#479) 2026-06-01 16:57:31 +02:00
Mike Klaas a38f3436f8 A few combat opcodes (#477)
* set_critter_burst_disable

* get/set/reset_critical_table

* `attack_is_aimed`
2026-05-31 16:28:00 -07:00
Mike Klaas 15c22ccdfc HRP/Sfall main menu scaling (#457)
Similar to HRP/Sfall, except:

    Does not support non-aspect-ratio-preserving scale mode
    Uses vanilla art if it fits without scaling (e.g. 640x480 or non-scaling res like 800x480). The scaled-down HR assets looks considerably worse.
2026-05-28 19:49:29 -07:00
Mike Klaas f263075abf Bundle Sfall's gl_highlighting.ssl (#461)
* Bundle Sfall's gl_highlight.ssl

Most mods/distributions assume this is exists since it is bundled with Sfall.  It's also a highly requested feature

I decided to merge this as a first-class game config instead of continuing to read mods/sfall-mods.ini.  I think most people to this to be a default capability that can be toggled in-engine.  Open to other thoughts on the matter.  Lost of people will already have sfall-mods.ini configured, which is a reason to keep it the way it was.

* PR feedback
2026-05-27 23:15:09 -07:00
Mike Klaas c95f91c2ff Fix returnval of critter_mod_skill (#476)
Fixes #474
2026-05-27 22:14:16 -07:00
Vlad K db86e53af2 Fix crash in opTokenize when prev is not null but not present in the source string (#473) 2026-05-27 18:40:54 +02:00
Vlad K 349af8fbb8 Fix aiPacketRead inducing crash when loading saves in certain order (#472)
* Fix aiPacketRead overwriting packet_num from savegames, leading to corrupted state on subsequent loads and crash

* Mark variable as unused and add comment
2026-05-27 10:21:45 +02:00
101 changed files with 5638 additions and 1197 deletions
+4
View File
@@ -614,6 +614,8 @@ if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
target_sources(mapper-ce PUBLIC
${FALLOUT_ENGINE_SOURCES}
${FALLOUT_PLATFORM_SOURCES}
"src/mapper/map_edge_setup.cc"
"src/mapper/map_edge_setup.h"
"src/mapper/map_func.cc"
"src/mapper/map_func.h"
"src/mapper/mapper.cc"
@@ -626,6 +628,8 @@ if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
"src/mapper/mp_scrpt.h"
"src/mapper/mp_targt.cc"
"src/mapper/mp_targt.h"
"src/mapper/mp_utils.cc"
"src/mapper/mp_utils.h"
"src/mapper/mp_text.cc"
"src/mapper/mp_text.h"
)
+13 -5
View File
@@ -76,7 +76,7 @@ For example:
## `ce-frm2png` CLI Tool
There is also a small FRM conversion tool in this repo for exporting Fallout `.frm` art to `.png`.
There is also a small FRM conversion tool in this repo for converting Fallout art between `.frm` and `.png`.
From the repo root, build it with your normal CMake workflow, targeting `ce-frm2png`:
@@ -89,18 +89,26 @@ Basic usage:
1. `./<BUILD_DIR>/ce-frm2png <input.frm> [output.png]`
2. `./<BUILD_DIR>/ce-frm2png <input.frm> [output.png] --palette <path-to-color.pal>`
3. `./<BUILD_DIR>/ce-frm2png <input.frm> [output.png] --frame <index> --direction <index>`
4. `./<BUILD_DIR>/ce-frm2png - [output.png|-] --palette <path-to-color.pal>`
4. `./<BUILD_DIR>/ce-frm2png <input.png> [output.frm] --palette <path-to-color.pal>`
5. `./<BUILD_DIR>/ce-frm2png <input.png> [output.frm] --fps 10 --action-frame 0 --x-offset 0 --y-offset 0`
6. `./<BUILD_DIR>/ce-frm2png <input.png> <output.png> --palette <path-to-color.pal>`
7. `./<BUILD_DIR>/ce-frm2png - [output.png|-] --from-frm --palette <path-to-color.pal>`
8. `./<BUILD_DIR>/ce-frm2png - [output.frm|-] --from-png --palette <path-to-color.pal>`
`-` can be used to take input from stdin (e.g. from `ce-dat-tool cat` or write to stdout).
`-` can be used to take input from stdin (e.g. from `ce-dat-tool cat`) or write to stdout. When reading from stdin, pass `--from-frm` or `--from-png`. Output defaults to PNG for FRM input and FRM for PNG input when output is omitted or `-`; use `--to-png` for PNG output to stdout from PNG input.
If `--palette` isn't specified, it will pick up `color.pal` in the .frm's directory or cwd.
Output file extensions are validated against the selected conversion. For example, PNG-to-FRM output must use `.frm`, and PNG output must use `.png`.
If `--palette` isn't specified, it will pick up `color.pal` in the input file's directory or cwd.
Notes:
- The tool expects a raw `color.pal` file. It does not read palettes directly from `master.dat`, so extract `color.pal` first if needed.
- A typical extraction flow is: `ce-dat-tool master.dat extract --lower . 'color.pal'`
- By default palette index `0` becomes transparent in the output PNG. Pass `--opaque` to keep it opaque.
- The current implementation writes RGBA PNGs. It does not preserve the original Fallout palette indices as a palette-indexed PNG.
- PNG output is always palette-indexed and preserves Fallout palette indices when possible.
- For `png -> frm`, transparent pixels (alpha below 128) map to palette index `0` by default. Pass `--opaque` to quantize every pixel instead.
- The current `png -> frm` implementation writes a single-frame FRM and points all six direction slots at the same frame payload.
## Updating SDL
+9
View File
@@ -153,6 +153,7 @@ In time this stuff will receive in-game interface, right now you have to do it m
* Auto open doors
* 44.1 kHz stereo sound/music supported, in .ogg and .wav format as well as legacy .acm
* Last used save slot is remembered
* Item/Corpse/Container/Critter highlighting (configure using [mods/sfall-mods.ini](https://github.com/sfall-team/sfall/blob/master/artifacts/config_files/sfall-mods.ini))
## Contributing
@@ -162,6 +163,14 @@ Integrating Sfall goodies is the top priority. Quality of life updates are OK to
For current sfall compatibility status and the remaining work needed to close gaps, see [SFALL_COMPATIBILITY.md](SFALL_COMPATIBILITY.md).
## For modders
* Robust (though not 100%) Sfall opcode and hook support
* .ogg/.wav support for map music
* .png support for static assets (must be 8bit indexed)
* .zip support for .dat archives
* Working BIS mapper
## License
The source code is this repository is available under the [Sustainable Use License](LICENSE.md).
+9 -7
View File
@@ -73,20 +73,22 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| 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 | âś… | - |
| 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 | âś… except available_global_script_types | - |
| 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: get_attack_type, get/set_bodypart_hit_modifier, combat_data, get/set_critter_current_ap, get/set_combat_free_move | - |
| 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 | âś… except block_combat, force_aimed_shots, disable_aimed_shots, get_last_target, get_last_attacker, set_spray_settings | - |
| 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<br>create_message_window | implemented: only message_box, get_text_width, show_window, create_message_window | - |
| 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<br>create_message_window | âś… except interface_print, draw_image, draw_image_scaled, interface_overlay | - |
| Interface / Outline | outlined_object<br>get_outline<br>set_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: critter_inven_obj2, item_weight | - |
| Interface / Inventory | display_stats<br>inventory_redraw<br>critter_inven_obj2<br>get_current_inven_size<br>item_weight | âś… except get_current_inven_size | - |
| Interface / Cursor | get/set_cursor_mode | âś… | - |
| Locks | lock_is_jammed<br>unjam_lock<br>set_unjam_locks_time | not implemented | - |
| INI settings | 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 get_ini_config, get_ini_config_db | `modified_ini` is intentionally omitted as deprecated. |
| INI settings | 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 | âś… | `modified_ini` is intentionally omitted as deprecated. |
| Objects and scripts | set_self<br>set_dude_obj<br>real_dude_obj<br>remove_script<br>get/set_script<br>obj_is_carrying_obj<br>loot_obj<br>dialog_obj<br>obj_under_cursor<br>get/set_object_data<br>get/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/set/remove_script, obj_is_carrying_obj, loot_obj, dialog_obj, obj_under_cursor, get_object_data, get_flags, set_flags, set_unique_id, obj_is_openable, get_proto_data, set_proto_data | - |
| Other / Game management | set_movie_path<br>stop/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, signal_close_game | - |
| Gameplay tweaks | 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>set_hp_per_level_mod<br>gdialog_get_barter_mod<br>get/set_unspent_ap_bonus<br>get/set_unspent_ap_perk_bonus<br>set_base_pickpocket_mod<br>set_critter_pickpocket_mod<br>get/set_inven_ap_cost<br>set_drugs_data<br>get_kill_counter<br>mod_kill_counter<br>set_pipboy_available | implemented: gdialog_get_barter_mod, get/set_unspent_ap{_perk}_bonus, get/set_inven_ap_cost | - |
| NPCs | inc_npc_level<br>get_npc_level<br>npc_engine_level_up | not implemented | - |
| Other | get_year<br>set_dm/df_model<br>active_hand<br>toggle_active_hand<br>get/set_viewport_x/y<br>hero_select_win<br>get_light_level<br>message_str_game<br>sneak_success<br>create_spatial<br>unwield_slot<br>add_g_timer_event<br>add_extra_msg_file<br>get_metarule_table<br>metarule_exist<br>remove_timer_event<br>spatial_radius | implemented: get_year, active_hand, toggle_active_hand, get_light_level, 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. |
| Hero Appearange | set_dm/df_model<br>hero_select_win | not implemented | - |
| Events | add_g_timer_event<br>remove_timer_event<br>create_spatial<br>spatial_radius | not implemented | - |
| Other | get_year<br>active_hand<br>toggle_active_hand<br>get/set_viewport_x/y<br>get_light_level<br>message_str_game<br>sneak_success<br>unwield_slot<br>add_extra_msg_file<br>get_metarule_table<br>metarule_exist<br> | âś… except get/set_viewport_x/y, sneak_success | `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
@@ -104,8 +106,8 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| 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` | đźš« | - |
| ItemDamage | `HOOK_ITEMDAMAGE` | âś… | - |
| MoveCost | `HOOK_MOVECOST` | âś… | - |
| AmmoCost | `HOOK_AMMOCOST` | âś… | Requires `check_weapon_ammo_cost=1` if you want pre-attack ammo validation to respect per-shot/per-round overrides. |
| KeyPress | `HOOK_KEYPRESS` | âś… | Third hook arg is currently `0`; CE doesn't use VK codes. |
| MouseClick | `HOOK_MOUSECLICK` | âś… | - |
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.
+12 -2
View File
@@ -119,15 +119,25 @@ numbers_in_dialogue=0
;Set to 2 to also skip the splash screen
skip_opening_movies=0
splash_screen_size=1
;Set to 1 to scale movies to fit the screen while preserving aspect ratio.
;Set to 0 to keep movies at their native size, centered on screen.
movie_aspect_fit=1
;Maximum number of columns shown in the main inventory and loot/steal windows (valid range: 1..2)
;2-column Loot/steal window requires minimum screen width of 673px
inventory_columns=2
;Set to 0 to keep the main menu art and controls at native size.
;Set to 1 to scale only the main menu background image to fit the screen while maintaining aspect ratio.
;Set to 2 to also scale the main menu button panel, red buttons, and button labels with the menu art.
main_menu_scale_mode=1
[qol]
; Automatically open doors that are unlocked and has no script attached.
auto_open_doors=0
; Overrides distance at which walk animation is used when using an object.
use_walk_distance=5
; Overrides distance at which walk animation is used when using an object. 0 disables walking
; This is the number of empty tiles between the player and the target
use_walk_distance=3
; Allow opening party member inventory from the long-press world menu.
party_trade_from_menu=1
; Allow switching to companions' inventories in loot screens (and barter, in the future)
@@ -60,12 +60,14 @@ procedure start begin
display_msg("Testing combat/inventory/object helpers...");
call assertEquals("metarule attack_is_aimed", metarule_exist("attack_is_aimed"), 1);
call assertEquals("metarule get_combat_free_move", metarule_exist("get_combat_free_move"), 1);
call assertEquals("metarule set_combat_free_move", metarule_exist("set_combat_free_move"), 1);
call assertEquals("metarule item_weight", metarule_exist("item_weight"), 1);
call assertEquals("metarule obj_is_openable", metarule_exist("obj_is_openable"), 1);
call assertEquals("opcode get_critter_current_ap", opcode_exists(OP_GET_CRITTER_CURRENT_AP), 1);
call assertEquals("opcode set_critter_current_ap", opcode_exists(OP_SET_CRITTER_CURRENT_AP), 1);
call assertEquals("attack_is_aimed defaults to false", attack_is_aimed, 0);
testItem := create_object_sid(TEST_ITEM_PID, 0, 0, -1);
call assertEquals("item_weight matches proto weight for test item", item_weight(testItem), obj_weight(testItem));
+6
View File
@@ -65,6 +65,12 @@ procedure ini_test_suite begin
call assertEquals("TC7 get_ini_sections size", len_array(result_array), 3);
call assertTrue("TC7 get_ini_sections", scan_array(result_array, "TestMisc") >= 0);
// get_ini_config
result_array := get_ini_config_db("config\\config_from_db.cfg");
call assertEquals("TC8 get_ini_config_db size", len_array(result_array), 2);
call assertTrue("TC8 get_ini_config ValidSection", result_array.ValidSection != 0);
call assertEquals("TC8 get_ini_config_db ValidValue", result_array.ValidSection.ValidKey, "ValidValue");
call report_test_results("ini");
end
+43
View File
@@ -0,0 +1,43 @@
#include "sfall.h"
#include "dik.h"
#include "define_lite.h"
#include "define_extra.h"
#include "test_utils.h"
procedure run_unwield_slot_test(variable slot, variable slotName) begin
variable equippedItem := critter_inven_obj2(dude_obj, slot);
if (equippedItem == 0) then begin
display_msg("unwield_slot " + slotName + ": slot is empty");
return;
end
unwield_slot(dude_obj, slot);
call assertTrue("unwield_slot clears " + slotName + " slot", critter_inven_obj2(dude_obj, slot) == 0);
call report_test_results("unwield_slot");
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_J) then begin
call run_unwield_slot_test(INVEN_TYPE_WORN, "armor");
end else if (key == DIK_K) then begin
call run_unwield_slot_test(INVEN_TYPE_RIGHT_HAND, "right");
end else if (key == DIK_L) then begin
call run_unwield_slot_test(INVEN_TYPE_LEFT_HAND, "left");
end
end
procedure start begin
if (not game_loaded) then return;
call assertEquals("unwield_slot metarule exists", metarule_exist("unwield_slot"), 1);
call assertEquals("critter_inven_obj2 metarule exists", metarule_exist("critter_inven_obj2"), 1);
display_msg("unwield_slot manual test ready: open inventory or barter, then press J armor, K right, L left");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
end
+98
View File
@@ -0,0 +1,98 @@
#include "sfall.h"
#include "dik.h"
#include "lib.arrays.h"
#include "../test_utils.h"
variable itemdamage_mode := 0;
variable itemdamage_seen := 0;
variable itemdamage_last_min := 0;
variable itemdamage_last_max := 0;
variable itemdamage_last_hit_mode := 0;
variable itemdamage_last_is_melee_weapon := 0;
variable itemdamage_last_weapon_pid := -1;
variable itemdamage_last_weapon_name := "<none>";
procedure itemdamage_mode_name begin
if (itemdamage_mode == 1) then return "fixed-42";
if (itemdamage_mode == 2) then return "range-30-30";
return "engine";
end
procedure itemdamage_weapon_name(variable weapon) begin
if (weapon == 0) then return "<unarmed>";
return obj_name(weapon);
end
procedure itemdamage_handler begin
variable
args := get_sfall_args,
weapon := args[2];
call assertEquals("itemdamage arg count", len_array(args), 6);
call assertTrue("itemdamage min <= max", args[0] <= args[1]);
call assertTrue("itemdamage melee flag", args[5] == 0 or args[5] == 1);
itemdamage_seen := itemdamage_seen + 1;
itemdamage_last_min := args[0];
itemdamage_last_max := args[1];
itemdamage_last_hit_mode := args[4];
itemdamage_last_is_melee_weapon := args[5];
itemdamage_last_weapon_name := itemdamage_weapon_name(weapon);
if (weapon == 0) then
itemdamage_last_weapon_pid := -1;
else
itemdamage_last_weapon_pid := obj_pid(weapon);
display_msg(string_format6("itemdamage mode=%s min=%d max=%d weapon=%s hit=%d melee=%d",
itemdamage_mode_name,
args[0],
args[1],
itemdamage_last_weapon_name,
itemdamage_last_hit_mode,
itemdamage_last_is_melee_weapon));
if (itemdamage_mode == 1) then begin
set_sfall_return(42);
end else if (itemdamage_mode == 2) then begin
set_sfall_return(30);
set_sfall_return(30);
end
end
procedure keypress_handler begin
variable
pressed := get_sfall_arg_at(0),
key := get_sfall_arg_at(1);
if (not pressed) then return;
if (key == DIK_J) then begin
itemdamage_mode := itemdamage_mode + 1;
if (itemdamage_mode > 2) then itemdamage_mode := 0;
display_msg(string_format1("itemdamage mode -> %s", itemdamage_mode_name));
end else if (key == DIK_K) then begin
display_msg(string_format7("itemdamage summary mode=%s seen=%d last=%d-%d weapon=%s pid=%d melee=%d",
itemdamage_mode_name,
itemdamage_seen,
itemdamage_last_min,
itemdamage_last_max,
itemdamage_last_weapon_name,
itemdamage_last_weapon_pid,
itemdamage_last_is_melee_weapon));
call assertTrue("itemdamage observed", itemdamage_seen > 0);
call report_test_results("itemdamage");
end
end
procedure start begin
if (not game_loaded) then return;
display_msg("itemdamage manual test ready:");
display_msg("press J to cycle engine/fixed-42/range-30-30");
display_msg("attack with fists, melee weapons, and ranged weapons");
display_msg("press K to report after HOOK_ITEMDAMAGE fires");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
register_hook_proc(HOOK_ITEMDAMAGE, itemdamage_handler);
end
+70
View File
@@ -0,0 +1,70 @@
#include "sfall.h"
#include "dik.h"
variable movecost_mode := 0;
procedure movecost_mode_name(variable mode) begin
variable name := "unknown";
if (mode == 0) then name := "engine";
else if (mode == 1) then name := "plus_per_hex";
else if (mode == 2) then name := "double";
return name;
end
procedure cycle_movecost_mode begin
if (movecost_mode == 0) then movecost_mode := 1;
else if (movecost_mode == 1) then movecost_mode := 2;
else movecost_mode := 0;
display_msg(string_format1("movecost mode=%s", movecost_mode_name(movecost_mode)));
end
procedure movecost_handler begin
variable
args := get_sfall_args,
critter := args[0],
distance := args[1],
defaultCost := args[2],
modeName := movecost_mode_name(movecost_mode),
newCost := defaultCost;
if (critter != dude_obj) then return;
display_msg(string_format3("movecost dist=%d default=%d mode=%s",
distance,
defaultCost,
modeName));
if (movecost_mode == 1) then begin
newCost := defaultCost + distance;
end else if (movecost_mode == 2) then begin
newCost := defaultCost * 2;
end else begin
return;
end
set_sfall_return(newCost);
end
procedure keypress_handler begin
variable
pressed := get_sfall_arg_at(0),
key := get_sfall_arg_at(1);
if (not pressed) then return;
if (key != DIK_M) then return;
call cycle_movecost_mode;
end
procedure start begin
if (not game_loaded) then return;
display_msg("movecost manual test ready: enter combat, hover a tile, then move");
display_msg("press M to cycle hook override: engine -> plus_per_hex -> double");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
register_hook_proc(HOOK_MOVECOST, movecost_handler);
end
@@ -0,0 +1,7 @@
[ValidSection]
ValidKey=ValidValue
Key2=2
[Section2]
Key1=AA
Key2=BB
+2
View File
@@ -0,0 +1,2 @@
A test mod for sfall scripting features testing.
Add it to your mods_order.txt
@@ -0,0 +1 @@
Put compiled scripts in here
+2
View File
@@ -1043,6 +1043,8 @@ int _action_climb_ladder(Object* critter, Object* ladder)
animationRequestOptions |= ANIMATION_REQUEST_NO_STAND;
reg_anim_begin(animationRequestOptions);
// note: distance is checked to ladder tile, but movement is to SE tile, so the walk/run choice
// could be slightly off
int tile = tileGetTileInDirection(ladder->tile, ROTATION_SE, 1);
if (actionPoints != -1 || objectWithinWalkDistance(critter, ladder)) {
animationRegisterMoveToTile(critter, tile, ladder->elevation, actionPoints, 0);
+300 -57
View File
@@ -1,12 +1,16 @@
#include "art.h"
#include <lodepng.h>
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "animation.h"
#include "content_config.h"
#include "datafile.h"
#include "debug.h"
#include "draw.h"
#include "game.h"
@@ -134,6 +138,7 @@ static int* gArtCritterFidShoudRunData;
static std::unordered_map<std::string, std::shared_ptr<NamedCacheEntry>> gNamedArtCache;
constexpr int kNamedCacheMaxBytes = 32 * 1024 * 1024; // 32MB soft limit
constexpr size_t kMaxNamedPngPixels = 16 * 1024 * 1024;
static unsigned int gNamedArtCacheMruCounter = 0;
static int gNamedArtCacheCurrentBytes = 0;
@@ -520,34 +525,6 @@ unsigned char* artLockFrameData(int fid, int frame, int direction, CacheEntry**
return nullptr;
}
// 0x4191CC
unsigned char* artLockFrameDataReturningSize(int fid, CacheEntry** handlePtr, int* widthPtr, int* heightPtr)
{
*handlePtr = nullptr;
Art* art = nullptr;
cacheLock(&gArtCache, fid, (void**)&art, handlePtr);
if (art == nullptr) {
return nullptr;
}
// NOTE: Uninline.
*widthPtr = artGetWidth(art, 0, 0);
if (*widthPtr == -1) {
return nullptr;
}
// NOTE: Uninline.
*heightPtr = artGetHeight(art, 0, 0);
if (*heightPtr == -1) {
return nullptr;
}
// NOTE: Uninline.
return artGetFrameData(art, 0, 0);
}
// 0x419260
int artUnlock(CacheEntry* handle)
{
@@ -1116,7 +1093,7 @@ static int artReadFrameData(unsigned char* data, File* stream, int count, int* p
// 0x419E1C
static int artReadHeader(Art* art, File* stream)
{
if (fileReadInt32(stream, &(art->field_0)) == -1) return -1;
if (fileReadInt32(stream, &(art->version)) == -1) return -1;
if (fileReadInt16(stream, &(art->framesPerSecond)) == -1) return -1;
if (fileReadInt16(stream, &(art->actionFrame)) == -1) return -1;
if (fileReadInt16(stream, &(art->frameCount)) == -1) return -1;
@@ -1133,13 +1110,208 @@ static int artReadHeader(Art* art, File* stream)
return 0;
}
// NOTE: Original function was slightly different, but never used. Basically
// it's a memory allocating variant of `artRead` (which reads data into given
// buffer). This function is useful to load custom `frm` files since `Art` now
// needs more memory then it's on-disk size (due to memory padding).
//
// 0x419EC0
Art* artLoad(const char* path)
static bool artPathHasExtension(const char* path, const char* extension)
{
size_t pathLength = strlen(path);
size_t extensionLength = strlen(extension);
if (pathLength < extensionLength) {
return false;
}
return compat_stricmp(path + pathLength - extensionLength, extension) == 0;
}
static bool artReadFile(const char* path, std::vector<unsigned char>& data)
{
int size = 0;
if (dbGetFileSize(path, &size) != 0 || size <= 0) {
return false;
}
data.resize(size);
return dbGetFileContents(path, data.data()) == 0;
}
static bool artUnpackIndexedPngPixels(const std::vector<unsigned char>& indexedData, unsigned width, unsigned height, unsigned bitdepth, unsigned char* output)
{
if (bitdepth != 8) {
return false;
}
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (indexedData.size() < pixelCount) {
return false;
}
memcpy(output, indexedData.data(), pixelCount);
return true;
}
static bool artValidateIndexedPngHeader(const char* path, unsigned width, unsigned height, const LodePNGColorMode& color)
{
if (color.colortype != LCT_PALETTE) {
debugPrint("ART: PNG is not palette-indexed: %s\n", path);
return false;
}
if (color.bitdepth != 8) {
debugPrint("ART: indexed PNG bit depth must be 8: %s\n", path);
return false;
}
if (width == 0 || height == 0 || width > SHRT_MAX || height > SHRT_MAX) {
debugPrint("ART: invalid indexed PNG dimensions for %s: %ux%u\n", path, width, height);
return false;
}
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (pixelCount > kMaxNamedPngPixels || pixelCount > INT_MAX) {
debugPrint("ART: indexed PNG is too large: %s\n", path);
return false;
}
return true;
}
static bool artIndexedPngHasSupportedTransparency(const LodePNGColorMode& color)
{
if (!lodepng_has_palette_alpha(&color)) {
return true;
}
for (size_t index = 0; index < color.palettesize; index++) {
unsigned char alpha = color.palette[index * 4 + 3];
if (index == 0) {
// palette index 0 is allowed to be either fully transparent or fully opaque
if (alpha != 0 && alpha != 255) {
return false;
}
} else if (alpha != 255) {
return false;
}
}
return true;
}
static Art* artAllocateSingleFrame(int width, int height, unsigned char** frameDataPtr)
{
if (width <= 0 || height <= 0 || width > SHRT_MAX || height > SHRT_MAX) {
return nullptr;
}
size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
if (pixelCount > kMaxNamedPngPixels || pixelCount > INT_MAX) {
return nullptr;
}
Art header = {};
header.version = 4;
header.framesPerSecond = 10;
header.actionFrame = 0;
header.frameCount = 1;
header.dataSize = sizeof(ArtFrame) + static_cast<int>(pixelCount);
int currentPadding = paddingForSize(sizeof(Art));
for (int rotation = 0; rotation < ROTATION_COUNT; rotation++) {
header.dataOffsets[rotation] = 0;
header.padding[rotation] = currentPadding;
}
int dataSize = artGetDataSize(&header);
unsigned char* data = reinterpret_cast<unsigned char*>(internal_malloc(dataSize));
if (data == nullptr) {
return nullptr;
}
memset(data, 0, dataSize);
Art* art = reinterpret_cast<Art*>(data);
*art = header;
ArtFrame* frame = reinterpret_cast<ArtFrame*>(data + sizeof(Art) + art->padding[0]);
frame->width = static_cast<short>(width);
frame->height = static_cast<short>(height);
frame->size = static_cast<int>(pixelCount);
frame->x = 0;
frame->y = 0;
*frameDataPtr = reinterpret_cast<unsigned char*>(frame) + sizeof(ArtFrame);
return art;
}
static Art* artLoadIndexedPng(const char* path)
{
std::vector<unsigned char> encoded;
if (!artReadFile(path, encoded)) {
return nullptr;
}
lodepng::State state;
state.decoder.color_convert = 0;
unsigned width = 0;
unsigned height = 0;
unsigned error = lodepng_inspect(&width, &height, &state, encoded.data(), encoded.size());
if (error != 0) {
debugPrint("ART: failed to inspect indexed PNG %s: %s\n", path, lodepng_error_text(error));
return nullptr;
}
if (!artValidateIndexedPngHeader(path, width, height, state.info_png.color)) {
return nullptr;
}
std::vector<unsigned char> indexedData;
error = lodepng::decode(indexedData, width, height, state, encoded);
if (error != 0) {
debugPrint("ART: failed to decode indexed PNG %s: %s\n", path, lodepng_error_text(error));
return nullptr;
}
if (!artIndexedPngHasSupportedTransparency(state.info_png.color)) {
debugPrint("ART: indexed PNG transparency is unsupported, reserve palette index 0 instead: %s\n", path);
return nullptr;
}
unsigned char* frameData = nullptr;
Art* art = artAllocateSingleFrame(static_cast<int>(width), static_cast<int>(height), &frameData);
if (art == nullptr) {
return nullptr;
}
if (!artUnpackIndexedPngPixels(indexedData, width, height, state.info_png.color.bitdepth, frameData)) {
debugPrint("ART: failed to read indexed PNG pixels: %s\n", path);
internal_free(art);
return nullptr;
}
return art;
}
static Art* artLoadPcx(const char* path)
{
char mutablePath[COMPAT_MAX_PATH];
strncpy(mutablePath, path, sizeof(mutablePath));
mutablePath[sizeof(mutablePath) - 1] = '\0';
int width = 0;
int height = 0;
unsigned char* pcxData = datafileRead(mutablePath, &width, &height);
if (pcxData == nullptr) {
return nullptr;
}
unsigned char* frameData = nullptr;
Art* art = artAllocateSingleFrame(width, height, &frameData);
if (art != nullptr) {
memcpy(frameData, pcxData, static_cast<size_t>(width) * static_cast<size_t>(height));
}
internal_free(pcxData);
return art;
}
static Art* artLoadFrm(const char* path)
{
File* stream = fileOpen(path, "rb");
if (stream == nullptr) {
@@ -1167,6 +1339,34 @@ Art* artLoad(const char* path)
return reinterpret_cast<Art*>(data);
}
// NOTE: Original function was slightly different, but never used. Basically
// it's a memory allocating variant of `artRead` (which reads data into given
// buffer). This function is useful to load custom `frm` files since `Art` now
// needs more memory then it's on-disk size (due to memory padding).
//
// 0x419EC0
Art* artLoad(const char* path)
{
if (path == nullptr) {
return nullptr;
}
if (artPathHasExtension(path, ".png")) {
return artLoadIndexedPng(path);
}
if (artPathHasExtension(path, ".pcx")) {
return artLoadPcx(path);
}
Art* art = artLoadFrm(path);
if (art != nullptr) {
return art;
}
return nullptr;
}
static Art* artLoadLocalized(const char* path)
{
const char* localizedPath;
@@ -1239,7 +1439,7 @@ int artWriteFrameData(unsigned char* data, File* stream, int count)
// 0x41A138
int artWriteHeader(Art* art, File* stream)
{
if (fileWriteInt32(stream, art->field_0) == -1) return -1;
if (fileWriteInt32(stream, art->version) == -1) return -1;
if (fileWriteInt16(stream, art->framesPerSecond) == -1) return -1;
if (fileWriteInt16(stream, art->actionFrame) == -1) return -1;
if (fileWriteInt16(stream, art->frameCount) == -1) return -1;
@@ -1310,7 +1510,7 @@ public:
const Art* art() const { return _art.get(); }
unsigned char* frameData(int frame, int direction, int& outWidth, int& outHeight) const;
ArtFrame* frame(int frame, int direction) const;
unsigned int mru = 0;
@@ -1323,17 +1523,9 @@ NamedCacheEntry::NamedCacheEntry(ArtPtr&& art)
{
}
unsigned char* NamedCacheEntry::frameData(int frame, int direction, int& outWidth, int& outHeight) const
ArtFrame* NamedCacheEntry::frame(int frame, int direction) const
{
unsigned char* data = artGetFrameData(_art.get(), frame, direction);
if (!data) {
outWidth = 0;
outHeight = 0;
return nullptr;
}
outWidth = artGetWidth(_art.get(), frame, direction);
outHeight = artGetHeight(_art.get(), frame, direction);
return data;
return artGetFrame(_art.get(), frame, direction);
}
std::shared_ptr<NamedCacheEntry> artLockNamedFrameData(const char* path)
@@ -1416,9 +1608,12 @@ ObjectType FrmId::objectType() const
FrmImage::FrmImage()
{
_key = nullptr;
_frame = nullptr;
_data = nullptr;
_width = 0;
_height = 0;
_xOffset = 0;
_yOffset = 0;
}
FrmImage::~FrmImage()
@@ -1429,9 +1624,12 @@ FrmImage::~FrmImage()
FrmImage::FrmImage(FrmImage&& other) noexcept
: _namedKey(std::move(other._namedKey))
, _key(other._key)
, _frame(other._frame)
, _data(other._data)
, _width(other._width)
, _height(other._height)
, _xOffset(other._xOffset)
, _yOffset(other._yOffset)
{
other.resetInternal();
}
@@ -1442,9 +1640,12 @@ FrmImage& FrmImage::operator=(FrmImage&& other) noexcept
unlock();
_namedKey = std::move(other._namedKey);
_key = other._key;
_frame = other._frame;
_data = other._data;
_width = other._width;
_height = other._height;
_xOffset = other._xOffset;
_yOffset = other._yOffset;
other.resetInternal();
}
@@ -1452,33 +1653,60 @@ FrmImage& FrmImage::operator=(FrmImage&& other) noexcept
}
bool FrmImage::lock(const FrmId& frmId)
{
return lock(frmId, 0, 0);
}
bool FrmImage::lock(const FrmId& frmId, int frame, int direction)
{
if (frmId.fid() >= 0) {
return lock(frmId.fid());
return lock(frmId.fid(), frame, direction);
}
if (frmId.filePath() != nullptr) {
return frmId.hasObjectType()
? lock(frmId.objectType(), frmId.filePath())
: lock(frmId.filePath());
? lock(frmId.objectType(), frmId.filePath(), frame, direction)
: lock(frmId.filePath(), frame, direction);
}
return false;
}
bool FrmImage::lock(unsigned int fid)
{
return lock(fid, 0, 0);
}
bool FrmImage::lock(unsigned int fid, int frame, int direction)
{
if (isLocked()) {
return false;
}
_data = artLockFrameDataReturningSize(fid, &_key, &_width, &_height);
if (!_data) {
Art* art = artLock(fid, &_key);
if (art == nullptr) {
return false;
}
_frame = artGetFrame(art, frame, direction);
if (_frame == nullptr) {
unlock();
return false;
}
_data = reinterpret_cast<unsigned char*>(_frame) + sizeof(ArtFrame);
_width = _frame->width;
_height = _frame->height;
_xOffset = _frame->x;
_yOffset = _frame->y;
return true;
}
bool FrmImage::lock(const char* frmPath)
{
return lock(frmPath, 0, 0);
}
bool FrmImage::lock(const char* frmPath, int frame, int direction)
{
if (isLocked()) {
return false;
@@ -1487,21 +1715,33 @@ bool FrmImage::lock(const char* frmPath)
_namedKey = artLockNamedFrameData(frmPath);
if (!_namedKey) return false;
_data = _namedKey->frameData(0, 0, _width, _height);
if (_data == nullptr) {
_namedKey = nullptr;
_frame = _namedKey->frame(frame, direction);
if (_frame == nullptr) {
unlock();
return false;
}
_data = reinterpret_cast<unsigned char*>(_frame) + sizeof(ArtFrame);
_width = _frame->width;
_height = _frame->height;
_xOffset = _frame->x;
_yOffset = _frame->y;
return true;
}
bool FrmImage::lock(ObjectType objType, const char* frmRelativePath)
{
return lock(objType, frmRelativePath, 0, 0);
}
bool FrmImage::lock(ObjectType objType, const char* frmRelativePath, int frame, int direction)
{
if (objType < OBJ_TYPE_ITEM || objType >= OBJ_TYPE_COUNT) {
return false;
}
snprintf(_art_name, sizeof(_art_name), "%s%s%s\\%s", _cd_path_base, "art\\", gArtListDescriptions[objType].name, frmRelativePath);
return lock(_art_name);
return lock(_art_name, frame, direction);
}
void FrmImage::unlock()
@@ -1516,9 +1756,12 @@ void FrmImage::resetInternal()
{
_namedKey = nullptr;
_key = nullptr;
_frame = nullptr;
_data = nullptr;
_width = 0;
_height = 0;
_xOffset = 0;
_yOffset = 0;
}
} // namespace fallout

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