Compare commits

..
Author SHA1 Message Date
Mike Klaas 7dab1e6219 Test fix web build mouse 2026-04-19 20:28:31 -07:00
Mike Klaas dcba8882bf Commandline .dat file viewer (#373) 2026-04-19 15:29:18 -07:00
Mike Klaas 62c2621fef Implement HOOK_ONDEATH (#384)
* Implement HOOK_ONDEATH

Test both combat death and script-triggered death

* move hp and DEAD flag above hook call
2026-04-19 14:24:51 -07:00
Vlad Kandgithub-actions[bot] 5188ebd4f4 Simplify settings registry with macros (#386)
* Simplify settings registry with macros

* chore: auto-format with clang-format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-17 22:44:23 +02:00
Mike Klaas 1a77361231 Implement HOOK_MOUSECLICK (#383)
Tested in all major UI modes (worldmap, inven, dialog, pipboy, &c.)
2026-04-17 08:54:22 -07:00
Mike Klaas ed2db51afd Implement gdialog_get_barter_mod (#385)
* Implement `gdialog_get_barter_mod`

Used by Et tu
2026-04-17 08:37:34 -07:00
Vlad K 53ff2293a1 Feature/sfall config move prefs (#381)
* ddraw.ini: moved all settings considered "player preference" into fallout2.cfg

* Update docs and config files

* Simplify docs

* Review fixes
2026-04-17 09:26:39 +00:00
Mike Klaas c4e722612f Match obj_is_openable semantics with Sfall (#382)
* Match `obj_is_openable` semantic with Sfall

Sfall implements this in the opcode: https://github.com/sfall-team/sfall/blob/master/sfall/FalloutEngine/EngineUtils.cpp#L232

This is also called in `objectOpenClose` to early exit when not openable, so I think it make sense to include in the engine-level check

* typo
2026-04-16 21:09:10 -07:00
Mike Klaas 8f3ccca71b Implement several trivial opcodes (#380)
* Implement several trivial opcodes

* get/set_combat_free_move
* item_weight
* obj_is_openable
* get/set_critter_current_ap
2026-04-16 09:20:56 -07:00
Mike Klaas 386b6c26c7 Implement HOOK_CALCAPCOST (#376)
* Implement HOOK_CALCAPCOST

An opcode a day keeps the stimpak away.

https://sfall-team.github.io/sfall/hook-types/#calcapcost

Same as Sfall, but also calls the hook on game load to display the correct AP cost on load.

Also fix a minor bug where the attack mode of the current hand sometimes leaked when loading a save game.  Normally attack mode (burst/reload/&c) isn't saved.  But if you are in aimed mode (say), and load a game with the same weapon equipped, the attack mode will persist.  This is pretty weird, so changed it to always be in the default mode when loading a save.

* test script

* PR comments

* doc tweaks
2026-04-16 05:18:12 +00:00
Vlad KandCopilot 51b0c3c8c0 Feature/cfg keep comments and order (#375)
* Retain key&sections comments when overwriting fallout2.cfg

- Vibe coded with human review and testing

* Fix anim_speed key name

* Revert comment removal by Gemini

* Handle final remove/rename failures

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

* Fix edge case when filePath is near MAX_PATH

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

* Remove dead weight

* Vibe coded bug fixes and new write flag

* Comment for config flags

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-15 21:14:00 +00:00
Mike Klaas 00a05e5279 get_outline and get_light_level (#379)
These are trivial.  I have a testing script but I don't think it's worth checking in.

(Used by FO2Tweaks)
2026-04-15 12:52:29 -07:00
Mike Klaas a65a426e27 Implement HOOK_WITHINPERCEPTION (#374)
* Implement HOOK_WITHIN_PERCEPTION

Another Et tu hook
2026-04-14 16:55:13 +00:00
Mike Klaasandgithub-actions[bot] a697f0b868 Fix garbled splash screen when SkipOpeningMovies=1 (#372)
* Fix garbled splash screen when `SkipOpeningMovies=1`

If the splash screen is shown but movies are skipped, the main menu palette is applied to the splash screen for a split second, showing garbled colours.  This doesn't happen with movies since they clear the screen.

Fix is to force a black screen clear beforing fading in the main menu

* simplify

* chore: auto-format with clang-format

* Restore palette before load-list error dialog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-13 20:17:15 -07:00
Vlad Kandgithub-actions[bot] 9e8eaa13f5 Settings registry refactoring (#371)
* Refactor settings save/load to use registry pattern

- Some AI work, needs review

* Fix player_speedup having incorrect cfg key, move config file names to cc, clang format code

* Missing for previous commit

* Remove config key aliases used only for registry, remove unused prefence names, some code cleanup after AI

* Move path normalization to settings.cc, skip mapper config outside of mapper,

* Simplify code by using template constructors

* Move ui_anim_speed to ui section

* chore: auto-format with clang-format

* Build fix attempt

* Format

* Compile fix attempt 2

* chore: auto-format with clang-format

* Code review fixes

* Init debug mode ASAP, log setting clamping, remove unused sound settings

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-14 00:43:41 +02:00
Vlad KandCopilot 14839db4f9 Set DPI Awareness hint on windows (#370)
* Set DPI Awareness hint on windows

* Windows specific hint must be used only on WIN32

* Check for SDL_HINT_WINDOWS_DPI_AWARENESS

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-13 00:41:33 +02:00
Mike Klaas e1f5f83ebd Fix strange mouse behaviour in windowed mode when scale>=2 (#369)
* Fix windowed mouse coordinate handling
2026-04-12 10:52:51 -07:00
Vlad K 6e6f82414e Update readme to reflect f2_res.ini migration, add fallout2.cfg to files (#368)
* Update readme to reflect f2_res.ini migration, add fallout2.cfg to files

* Better defaults, valid example resolution, explain scaling
2026-04-11 22:37:22 +02:00
65 changed files with 2065 additions and 918 deletions
+1
View File
@@ -10,6 +10,7 @@
*.userosscache
*.sln.docstates
.DS_Store
.local-tools/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
+22
View File
@@ -446,6 +446,28 @@ target_include_directories(${EXECUTABLE_NAME} PRIVATE ${ZLIB_INCLUDE_DIRS})
target_link_libraries(${EXECUTABLE_NAME} ${SDL2_LIBRARIES})
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${SDL2_INCLUDE_DIRS})
add_executable(fallout2-dat
"tools/dat_tool.cc"
"src/dfile.cc"
"src/platform_compat.cc"
)
if(APPLE)
set_target_properties(fallout2-dat PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
)
endif()
target_link_libraries(fallout2-dat
fpattern_windows::fpattern_windows
${ZLIB_LIBRARIES}
${SDL2_LIBRARIES}
)
target_include_directories(fallout2-dat PRIVATE "src")
target_include_directories(fallout2-dat PRIVATE ${ZLIB_INCLUDE_DIRS})
target_include_directories(fallout2-dat PRIVATE ${SDL2_INCLUDE_DIRS})
if(APPLE)
if(IOS)
install(TARGETS ${EXECUTABLE_NAME} DESTINATION "Payload")
-3
View File
@@ -119,9 +119,6 @@
"base"
],
"generator": "Xcode",
"cacheVariables": {
"CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY": ""
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
+20
View File
@@ -2,6 +2,26 @@
This file is currently a shell to be filled in over time. For now, it only captures project-specific maintenance notes that are easy to miss.
## `.dat` CLI Tool
There is a small command-line archive tool in this repo for inspecting Fallout `.dat` files using the same reader implementation as the game.
Build it with `make TARGET=fallout2-dat`.
The executable is written to your selected `BUILD_DIR`, so the default path is `out/build/local-debug-arm64/fallout2-dat` on this macOS setup, but other platforms or custom build directories will differ.
The current tool is read-only. Available commands:
1. `./<BUILD_DIR>/fallout2-dat <archive.dat> list [pattern]`
2. `./<BUILD_DIR>/fallout2-dat <archive.dat> info [pattern]`
3. `./<BUILD_DIR>/fallout2-dat <archive.dat> extract [--lower] <output-dir> [pattern]`
4. `./<BUILD_DIR>/fallout2-dat <archive.dat> cat <entry>`
Use `--lower` with `extract` when you want every extracted file and directory name forced to lowercase.
For example, from `/Applications/Fallout2Codex` you can run:
`/Users/klaas/game/fallout2-ce/out/build/local-debug-arm64/fallout2-dat master.dat extract --lower /tmp/fallout2-dat-lower data\\*`
## Updating SDL
SDL is pinned for native builds in `third_party/sdl2/CMakeLists.txt`. Right now, Android also relies on checked-in Java bindings in `os/android/app/src/main/java/org/libsdl/app`, and those bindings must match the SDL version fetched by CMake.
+20 -44
View File
@@ -84,7 +84,7 @@ $ mv app /Applications/Fallout2
### Browser
> **NOTE**: WebAssebly build with emscripten
> **NOTE**: WebAssembly build with emscripten
```
docker run --rm -v $(pwd):/src emscripten/emsdk:3.1.74 sh -c 'git config --global --add safe.directory "*" && mkdir -p build && cd build && emcmake cmake ../ -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain/Emscripten.cmake && emmake make'
```
@@ -96,59 +96,35 @@ The main configuration file is `fallout2.cfg`. There are several important setti
The `sound` folder (with `music` folder inside) might be located either in `data` folder, or be in the Fallout folder. Update `music_path1` setting to match your hierarchy, usually it's `data/sound/music/` or `sound/music/`. Make sure it matches your path exactly (so it might be `SOUND/MUSIC/` if you've installed Fallout from CD). Music files themselves (with `ACM` extension) should be all uppercased, regardless of `sound` and `music` folders.
The second configuration file is `f2_res.ini`. Use it to change game window size, enable/disable fullscreen mode and configure IFACE settings (the control bar at the bottom of the game screen).
Additional settings for screen resolution, UI customization, and map options are now integrated into the main `fallout2.cfg` file (previously part of `f2_res.ini` from Mash's HRP). When Fallout 2 CE starts, if it detects an existing `f2_res.ini` file, it automatically migrates these settings into `fallout2.cfg`. After migration, `fallout2.cfg` becomes the single source of truth for this configuration.
The following settings can be configured in `fallout2.cfg` under the `[screen]` and `[ui]` sections:
```ini
[MAIN]
SCR_WIDTH=1280
SCR_HEIGHT=720
WINDOWED=1 ; 0 = fullscreen
SCALE_2X=1 ; 0 = original scale, 1 = 2x - requires increasing the minimum resolution from 640x480 to 1280x960.
[screen]
resolution_x=1600 ; actual game window size (screen resolution), in pixels
resolution_y=1080
windowed=1 ; 0 = fullscreen
scale=2 ; 1 = original scale, 2 = 2x scale, etc. (e.g. at scale 2 and screen resolution 1920x1080, in-game resolution will be 960x540, thus every pixel is twice as wide and tall)
[IFACE]
; if IFACE_BAR_MODE=0 - the bottom of the map view window sits at the top of the IFACE Bar.
; if IFACE_BAR_MODE=1 - the bottom of the map view window extends to the base of the screen and is overlapped by the IFACE Bar.
IFACE_BAR_MODE=0
;if IFACE_BAR_SIDE_ART=0 - Black, No Iface-bar side art used.
;if IFACE_BAR_SIDE_ART=1 - Metal (grey) look Iface-bar side art used.
;if IFACE_BAR_SIDE_ART=2 - Leather look Iface-bar side art used.
;if IFACE_BAR_SIDE_ART=3-6 - Alternative Metal look Iface-bar side art used.
;if IFACE_BAR_SIDE_ART=7 - Alternative Leather look Iface-bar side art used.
;if IFACE_BAR_SIDE_ART=8 - Metal (brown) look Iface-bar side art used.
IFACE_BAR_SIDE_ART=2
;if IFACE_BAR_SIDES_ORI=0 - Iface-bar side graphics extend from the Iface-Bar to the Screen edges.
;if IFACE_BAR_SIDES_ORI=1 - Iface-bar side graphics extend from the Screen edges to the Iface-Bar.
IFACE_BAR_SIDES_ORI=0
;This will increase the width of the interface bar expanding the area used to display text.
;if IFACE_BAR_WIDTH=640 - Interface bar will remain at it's original width.
;if IFACE_BAR_WIDTH=800 - Interface bar will use 800pix wide asset from f2_res.dat.
;IFACE_BAR_WIDTH=640
[STATIC_SCREENS]
;if SPLASH_SCRN_SIZE=0 - Splash screen shows at original size if it fits, otherwise scales down while preserving aspect ratio.
;if SPLASH_SCRN_SIZE=1 - Splash screen scales to fit the screen while preserving aspect ratio.
;if SPLASH_SCRN_SIZE=2 - Splash screen stretches to fill the entire screen.
SPLASH_SCRN_SIZE=0
[MAPS]
;if IGNORE_MAP_EDGES=0 - Hi-Res map scroll edges are enabled.
;if IGNORE_MAP_EDGES=1 - Hi-Res map scroll edges are ignored.
IGNORE_MAP_EDGES=0
[ui]
iface_bar_mode=0 ; 0 = interface bar below game window, 1 = interface bar overlaps game window
iface_bar_width=800 ; Width of interface bar (640 = original, 800 = extended)
iface_bar_side_art=2 ; Interface bar side art style (0=black, 1=metal grey, 2=leather, 3-8=alternative styles)
iface_bar_sides_ori=0 ; Side graphics orientation (0=extend from bar to edges, 1=extend from edges to bar)
splash_screen_size=1 ; Splash screen scaling (0=original size, 1=fit preserving aspect, 2=stretch to fill)
ignore_map_edges=0 ; Hi-res map scroll edges (0=enabled, 1=ignored)
```
Recommendations:
**Recommendations:**
- **Desktops**: Use any size you see fit.
- **Tablets**: Set these values to logical resolution of your device, for example iPad Pro 11 is 1668x2388 (pixels), but it's logical resolution is 834x1194 (points).
- **Mobile phones**: Set height to 480, calculate width according to your device screen (aspect) ratio, for example Samsung S21 is 20:9 device, so the width should be 480 * 20 / 9 = 1067.
In time this stuff will receive in-game interface, right now you have to do it manually. For a sample f2_res.ini configuration file, containing all currently working settings use this link: [f2_res.ini](https://raw.githubusercontent.com/fallout2-ce/fallout2-ce/refs/heads/mainmenu/files/f2_res.ini)
In time this stuff will receive in-game interface, right now you have to do it manually. To see all currently working fallout2.cfg settings, just run the game once and quit. It will be automatically updated with defaults for every supported setting.
*Note*: Use of the IFACE_BAR settings requires the `f2_res.dat` file, which contains graphical assets. Various versions are available, but one compatible with the above settings can be found here: [f2_res.dat](https://github.com/fallout2-ce/fallout2-ce/raw/refs/heads/main/files/f2_res.dat)
*Note*: use of the IFACE_BAR settings requires the f2_res.dat file, which contains graphical assets. Various versions are available, but one compatible with the above f2_res.ini be found here: [f2_res.dat](https://github.com/fallout2-ce/fallout2-ce/raw/refs/heads/mainmenu/files/f2_res.dat)
The third configuration file is `ddraw.ini` (part of Sfall). There are dozens of options that adjust or override engine behaviour and gameplay mechanics. This file is intended for modders and advanced users.
The second configuration file is `ddraw.ini` (part of Sfall). There are dozens of options that adjust or override engine behaviour and gameplay mechanics. This file is intended for modders and advanced users.
For a sample ddraw.ini configuration file, containing all currently working settings use this link: [ddraw.ini](https://raw.githubusercontent.com/fallout2-ce/fallout2-ce/refs/heads/main/files/ddraw.ini)
+30 -12
View File
@@ -4,6 +4,24 @@ This document tracks Fallout 2 CE compatibility with sfall. This is for modders
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.
## Settings (ddraw.ini → fallout2.cfg)
Some settings previously read from `ddraw.ini` have been moved into `fallout2.cfg`. The table below maps each setting to its new location. `HiResMode` had no effect in CE and has been removed.
| ddraw.ini section | ddraw.ini key | fallout2.cfg section | fallout2.cfg key |
| --- | --- | --- | --- |
| `Misc` | `SkipOpeningMovies` | `ui` | `skip_opening_movies` |
| `Misc` | `DisplayKarmaChanges` | `ui` | `display_karma_changes` |
| `Misc` | `DisplayBonusDamage` | `ui` | `display_bonus_damage` |
| `Misc` | `NumbersInDialogue` | `ui` | `numbers_in_dialogue` |
| `Misc` | `AutoQuickSave` | `ui` | `auto_quick_save` |
| `Main` | `EnableHighResolutionStencil` | `ui` | `enable_high_resolution_stencil` |
| `Misc` | `ConsoleOutputPath` | `debug` | `console_output_path` |
| `Misc` | `GaplessMusic` | `sound` | `gapless_music` |
| `Misc` | `ScreenshotsFormat` | `system` | `screenshots_format` |
| `Misc` | `UseWalkDistance` | `qol` | `use_walk_distance` |
| `Misc` | `AutoOpenDoors` | `qol` | `auto_open_doors` |
## Opcodes / Metarules
See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/) for documentation on specific functions.
@@ -16,7 +34,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| 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 | get_perk_owed<br>set_perk_owed<br>set_perk_image<br>set_perk_*<br>set_pyromaniac_mod<br>apply_heaveho_fix<br>set_swiftlearner_mod<br>has/set_fake_perk<br>has/set_fake_trait<br>set_selectable_perk<br>set_perkbox_title<br>show/hide_real_perks<br>perk_add_mode<br>clear_selectable_perks<br>add/remove_trait<br>seq_perk_freq | implemented: only get_perk_owed, set_perk_owed | - |
| Perks and traits | set_perk_image<br>set_perk_*<br>set_pyromaniac_mod<br>apply_heaveho_fix<br>set_swiftlearner_mod<br>has/set_fake_perk<br>has/set_fake_trait<br>set_selectable_perk<br>set_perkbox_title<br>show/hide_real_perks<br>perk_add_mode<br>clear_selectable_perks<br>add/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 | - |
@@ -39,20 +57,20 @@ 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 | âś… 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 | âś… 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: only get_attack_type, get_bodypart_hit_modifier, combat_data, set_bodypart_hit_modifier | - |
| 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_bodypart_hit_modifier, combat_data, set_bodypart_hit_modifier, get_critter_current_ap, set_critter_current_ap, get_combat_free_move, set_combat_free_move | - |
| 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 / Outline | outlined_object<br>get_outline<br>set_outline | âś… except get_outline | - |
| 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: only critter_inven_obj2 | - |
| 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 / 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. |
| 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, obj_is_carrying_obj, 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, 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>get_unspent_ap_bonus<br>gdialog_get_barter_mod<br>set_unspent_ap_bonus<br>get/set_unspent_ap_perk_bonus<br>set_inven_ap_cost<br>set_base_pickpocket_mod<br>set_critter_pickpocket_mod<br>get_inven_ap_cost<br>set_drugs_data<br>get_kill_counter<br>mod_kill_counter<br>set_pipboy_available | not implemented | - |
| 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_script, obj_is_carrying_obj, loot_obj, dialog_obj, obj_under_cursor, get_object_data, get_flags, set_flags, 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>get_unspent_ap_bonus<br>gdialog_get_barter_mod<br>set_unspent_ap_bonus<br>get/set_unspent_ap_perk_bonus<br>set_inven_ap_cost<br>set_base_pickpocket_mod<br>set_critter_pickpocket_mod<br>get_inven_ap_cost<br>set_drugs_data<br>get_kill_counter<br>mod_kill_counter<br>set_pipboy_available | implemented: gdialog_get_barter_mod | - |
| 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, 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. |
| 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. |
## Hooks
@@ -60,11 +78,11 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| --- | --- | --- | --- |
| ToHit | `HOOK_TOHIT` | âś… | - |
| AfterHitRoll | `HOOK_AFTERHITROLL` | đźš« | Et tu |
| CalcAPCost | `HOOK_CALCAPCOST` | đźš« | - |
| CalcAPCost | `HOOK_CALCAPCOST` | âś… | - |
| DeathAnim1 | `HOOK_DEATHANIM1` | đźš« | Use DEATHANIM2 instead |
| DeathAnim2 | `HOOK_DEATHANIM2` | âś… | - |
| CombatDamage | `HOOK_COMBATDAMAGE` | âś… | - |
| OnDeath | `HOOK_ONDEATH` | đźš« | - |
| 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. |
@@ -74,10 +92,10 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| ItemDamage | `HOOK_ITEMDAMAGE` | đźš« | - |
| AmmoCost | `HOOK_AMMOCOST` | đźš« | Et tu |
| KeyPress | `HOOK_KEYPRESS` | âś… | Third hook arg is currently `0`; CE doesn't use VK codes. |
| MouseClick | `HOOK_MOUSECLICK` | đźš« | - |
| MouseClick | `HOOK_MOUSECLICK` | âś… | - |
| UseSkill | `HOOK_USESKILL` | đźš« | - |
| Steal | `HOOK_STEAL` | đźš« | Et tu |
| WithinPerception | `HOOK_WITHINPERCEPTION` | đźš« | Et tu |
| WithinPerception | `HOOK_WITHINPERCEPTION` | âś… | - |
| InventoryMove | `HOOK_INVENTORYMOVE` | âś… | - |
| InvenWield | `HOOK_INVENWIELD` | đźš« | - |
| AdjustFID | `HOOK_ADJUSTFID` | đźš« | - |
-26
View File
@@ -3,10 +3,6 @@
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[Main]
;Set to one to hide areas outside map bounds when using higher than 640x480 resolution, and to zero to disable
;EnableHighResolutionStencil=1
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[Misc]
@@ -72,13 +68,6 @@ TownMapHotkeysFix=1
;Must be non-zero to use the edit/get/reset_critical script functions
;OverrideCriticalTable=2
;Set to 1 to get notification of karma changes in the notification window
DisplayKarmaChanges=0
;Set to 1 to skip the 3 opening movies
;Set to 2 to also skip the splash screen
SkipOpeningMovies=0
;Change the Skilldex cursor FRM numbers
;Default is 293 for all skills
Lockpick=293
@@ -99,18 +88,9 @@ Repair=293
;Syntax is 'city id:global id', with each city/global pair separated by a comma.
;CityRepsList=0:5001,2:5003,3:5004,4:5005,5:5006,6:5007,7:5008,8:5009,10:5011,11:5012,60:1640
;Set this to a valid path to save a copy of the console contents
;ConsoleOutputPath=console.txt
;Set to 1 to add additional pages of save slots !!! can be implemented !!!
;ExtraSaveSlots=1
;To use more than one save slot for quick saving (F6 key) without picking a slot beforehand, set the next two lines
;Quick save will cyclically overwrite saves from the first slot on the specified page to the last slot on the n-th page
;AutoQuickSave sets how many pages you want to use for quick saving (valid range: 1..10)
;Set to 0 to disable
AutoQuickSave=0
;These lines allow you to control the karma FRMs displayed on the character screen
;Number of KarmaPoints should be 1 less than number of KarmaFRMs
;KarmaFRMs=47,48,49
@@ -122,9 +102,6 @@ ScienceOnCritters=0
;Set to 1 to fix the bug that caused bonus HtH damage to not be applied correctly.
BonusHtHDamageFix=1
;Set to 1 to display additional points of damage from Bonus HtH/Ranged Damage perks in the inventory
;DisplayBonusDamage=1
;Set to 1 to remove the limits that stop the game from rolling critical successes/failures in the first few days of game time
RemoveCriticalTimelimits=0
@@ -170,9 +147,6 @@ RemoveCriticalTimelimits=0
;You need to use the message_str_game script function to get messages from the files
;ExtraGameMsgFileList=
;Set to 1 to display numbered dialogue options
NumbersInDialogue=0
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
; Configuration ini files
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+115
View File
@@ -0,0 +1,115 @@
[debug]
;Set this to a valid path to save a copy of the console contents
console_output_path=
mode=log
output_map_data_info=1
show_load_info=1
show_script_messages=1
show_tile_num=1
window_height=192
window_width=300
[mapper]
default_f8_as_game=1
fix_map_inventory=0
fix_map_objects=0
ignore_rebuild_errors=0
librarian=0
override_librarian=0
rebuild_protos=0
run_mapper_as_game=0
save_text_maps=0
show_pid_numbers=0
sort_script_list=0
use_art_not_protos=0
[preferences]
brightness=1.000000
combat_difficulty=1
combat_looks=1
combat_messages=1
combat_speed=50
combat_taunts=1
game_difficulty=1
item_highlight=1
language_filter=0
mouse_sensitivity=1.000000
player_speedup=0
running=1
running_burning_guy=1
subtitles=1
target_highlight=1
text_base_delay=6.000000
text_line_delay=2.000000
violence_level=3
[screen]
resolution_x=640
resolution_y=480
scale=1
windowed=1
[sound]
cache_size=448
debug=1
debug_sfxc=1
gapless_music=1
initialize=1
master_volume=22281
music=1
music_path1=data\sound\music\
music_path2=sound\music\
music_volume=22281
sndfx_volume=22281
sounds=1
speech=1
speech_volume=22281
[system]
art_cache_size=8
color_cycling=1
critter_dat=critter.dat
critter_patches=data
cycle_speed_factor=1
executable=game
free_space=20480
hashing=1
interrupt_walk=1
language=english
master_dat=master.dat
master_patches=data
screenshots_format=png
scroll_lock=0
splash=5
[ui]
;Speed up or slow down all UI animations such as screen fade, dialog window show/hide, AC numbers roll, etc.
anim_speed=2.000000
;To use more than one save slot for quick saving (F6 key) without picking a slot beforehand
;Quick save will cyclically overwrite saves from the first slot on the specified page to the last slot on the n-th page
;auto_quick_save sets how many pages you want to use for quick saving (valid range: 1..10)
;Set to 0 to disable
auto_quick_save=3
;Set to 1 to display additional points of damage from Bonus HtH/Ranged Damage perks in the inventory
display_bonus_damage=1
;Set to 1 to get notification of karma changes in the notification window
display_karma_changes=0
;Set to one to hide areas outside map bounds when using higher than 640x480 resolution, and to zero to disable
enable_high_resolution_stencil=1
iface_bar_mode=0
iface_bar_side_art=2
iface_bar_sides_ori=0
iface_bar_width=800
ignore_map_edges=0
;Set to 1 to display numbered dialogue options
numbers_in_dialogue=0
;Set to 1 to skip the 3 opening movies
;Set to 2 to also skip the splash screen
skip_opening_movies=0
splash_screen_size=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
@@ -0,0 +1,80 @@
#include "sfall.h"
#include "dik.h"
#include "define_lite.h"
#include "command_lite.h"
#include "test_utils.h"
#define TEST_ITEM_PID 40
#define OP_GET_CRITTER_CURRENT_AP 0x8191
#define OP_SET_CRITTER_CURRENT_AP 0x8192
#define opcode_exists(opcode) sfall_func1("opcode_exists", opcode)
procedure run_combat_turn_roundtrip begin
variable originalAp := get_critter_current_ap(dude_obj);
variable originalFreeMove := get_combat_free_move;
variable expectedAp := originalAp + 1;
variable expectedFreeMove := originalFreeMove + 2;
set_critter_current_ap(dude_obj, expectedAp);
set_combat_free_move(expectedFreeMove);
call assertEquals("set/get_critter_current_ap roundtrip", get_critter_current_ap(dude_obj), expectedAp);
call assertEquals("set/get_combat_free_move roundtrip", get_combat_free_move, expectedFreeMove);
set_critter_current_ap(dude_obj, originalAp);
set_combat_free_move(originalFreeMove);
call assertEquals("get_critter_current_ap restored", get_critter_current_ap(dude_obj), originalAp);
call assertEquals("get_combat_free_move restored", get_combat_free_move, originalFreeMove);
call report_test_results("combat_inventory_misc_combat");
end
procedure report_obj_under_cursor begin
variable target := obj_under_cursor(0, 1);
if (target == 0) then begin
display_msg("No object under cursor.");
return;
end
display_msg("Cursor object: " + obj_name(target) + ", openable=" + obj_is_openable(target));
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_combat_turn_roundtrip();
end else if (key == DIK_K) then begin
call report_obj_under_cursor();
end
end
procedure start begin
variable testItem;
if (not game_loaded) then return;
display_msg("Testing combat/inventory/object helpers...");
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);
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));
call assertEquals("non-container item is not openable", obj_is_openable(testItem), 0);
destroy_object(testItem);
call report_test_results("combat_inventory_misc");
display_msg("Manual test: enter combat, wait for your turn, then press J to roundtrip AP/free-move.");
display_msg("Manual test: point at a door or container and press K to print obj_is_openable().");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
end
+64
View File
@@ -0,0 +1,64 @@
#include "sfall.h"
#include "dik.h"
#include "lib.arrays.h"
#include "lib.obj.h"
variable calcapcost_override_enabled := true;
procedure calcapcost_mode_name begin
if (calcapcost_override_enabled) then return "override";
return "engine";
end
procedure calcapcost_item_name(variable item) begin
if (item == 0) then return "<null>";
return obj_name(item);
end
procedure calcapcost_handler begin
variable
args := get_sfall_args,
critter := args[0],
attackType := args[1],
isAimed := args[2],
defaultCost := args[3],
weapon := args[4],
weaponPid := 0;
if (critter != dude_obj) then return;
if (weapon) then weaponPid := obj_pid(weapon);
display_msg(string_format("calcapcost mode=%s attack=%d aimed=%d default=%d weapon=%s pid=%d",
calcapcost_mode_name,
attackType,
isAimed,
defaultCost,
calcapcost_item_name(weapon),
weaponPid));
if (calcapcost_override_enabled) then
set_sfall_return(defaultCost + 3);
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_H) then return;
calcapcost_override_enabled := not calcapcost_override_enabled;
display_msg(string_format1("calcapcost mode -> %s", calcapcost_mode_name));
intface_redraw;
end
procedure start begin
if (not game_loaded) then return;
display_msg("calcapcost manual test ready: press H to toggle override (+3 AP)");
display_msg("check weapon, unarmed, and non-weapon hand items; hook logs args for dude only");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
register_hook_proc(HOOK_CALCAPCOST, calcapcost_handler);
end
+52
View File
@@ -0,0 +1,52 @@
#include "sfall.h"
#include "dik.h"
#include "define_lite.h"
#define TEST_KEY (DIK_J)
procedure ondeath_handler begin
variable
dead_critter := get_sfall_arg_at(0),
critter_name := "<null>",
critter_pid := -1;
if (dead_critter) then begin
critter_name := obj_name(dead_critter);
critter_pid := obj_pid(dead_critter);
end
display_msg(string_format2("ondeath critter=%s pid=%d", critter_name, critter_pid));
end
procedure keypress_handler begin
variable
pressed := get_sfall_arg_at(0),
key := get_sfall_arg_at(1),
target;
if (not pressed) then return;
if (key != TEST_KEY) then return;
target := obj_under_cursor(true, false);
if (target == 0) then begin
display_msg("ondeath test: no critter under cursor");
return;
end
if (target == dude_obj) then begin
display_msg("ondeath test: refusing to kill dude");
return;
end
display_msg(string_format1("ondeath test: killing %s", obj_name(target)));
critter_dmg(target, 9999, DMG_BYPASS_ARMOR);
end
procedure start begin
if (not game_loaded) then return;
display_msg("ondeath manual test ready: hover a critter and press J");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
register_hook_proc(HOOK_ONDEATH, ondeath_handler);
end
+105
View File
@@ -0,0 +1,105 @@
#include "sfall.h"
#include "dik.h"
#include "lib.arrays.h"
#include "lib.obj.h"
variable withinperception_override := -1;
procedure withinperception_override_name(variable value) begin
if (value == -1) then return "engine";
if (value == 0) then return "block";
if (value == 1) then return "allow";
if (value == 2) then return "force_detect";
return "unknown";
end
procedure withinperception_type_name(variable hook_type) begin
if (hook_type == 0) then return "engine";
if (hook_type == 1) then return "see";
if (hook_type == 2) then return "hear";
if (hook_type == 3) then return "ai_target";
return "unknown";
end
procedure cycle_override begin
if (withinperception_override == -1) then withinperception_override := 0;
else if (withinperception_override == 0) then withinperception_override := 1;
else if (withinperception_override == 1) then withinperception_override := 2;
else withinperception_override := -1;
display_msg(string_format1("withinperception override=%s", withinperception_override_name(withinperception_override)));
end
procedure print_target_checks begin
variable
target := obj_under_cursor(true, false),
canSee,
canHear,
target_name := "(null)";
target_name := obj_name_safe(target);
display_msg(string_format2("withinperception target=%s pid=%d", target_name, target and obj_pid(target)));
if (target == 0) then begin
display_msg("withinperception no target under cursor");
return;
end
canSee := obj_can_see_obj(dude_obj, target);
canHear := obj_can_hear_obj(dude_obj, target);
display_msg(string_format2("withinperception see=%d hear=%d", canSee, canHear));
end
procedure withinperception_handler begin
variable
args := get_sfall_args,
watcher := args[0],
target := args[1],
original_result := args[2],
hook_type := args[3],
watcher_name := "(null)",
target_name := "(null)",
override_result := withinperception_override;
watcher_name := obj_name_safe(watcher);
target_name := obj_name_safe(target);
if (watcher == dude_obj) then begin
display_msg(string_format("withinperception %s %s -> %s [res=%d]",
withinperception_type_name(hook_type),
watcher_name,
target_name, original_result));
end
if (override_result == -1) then return;
if (hook_type != 1 and override_result == 2) then override_result := 1;
set_sfall_return(override_result);
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
call print_target_checks;
end else if (key == DIK_K) then begin
call cycle_override;
end
end
procedure start begin
if (not game_loaded) then return;
display_msg("withinperception manual test ready: J checks target under cursor, K cycles override");
display_msg("withinperception override order: engine -> block -> allow -> force_detect");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
register_hook_proc(HOOK_WITHINPERCEPTION, withinperception_handler);
end
+32 -152
View File
@@ -37,7 +37,6 @@
#include "platform_compat.h"
#include "proto.h"
#include "scripts.h"
#include "settings.h"
#include "sfall_config.h"
#include "skill.h"
#include "stat.h"
@@ -145,11 +144,6 @@ enum {
EDITOR_BUTTONS_COUNT = EDITOR_FIRST_TRAIT + TRAIT_COUNT,
};
static constexpr int kMaxPerkOwed = 250;
// nb. this is number of perks, not total perk ranks. This only prevents level ups from granting more perks.
static constexpr int kMaxSelectablePerks = 37;
static constexpr int kPrimaryStatDescriptionWidth = 100;
enum {
EDITOR_GRAPHIC_BIG_NUMBERS,
EDITOR_GRAPHIC_AGE_MASK,
@@ -288,8 +282,6 @@ static int perkDialogDrawTraits(int a1);
static int perkDialogOptionCompare(const void* a1, const void* a2);
static int perkDialogDrawCard(int frmId, const char* name, const char* rank, char* description);
static void _pop_perks();
static int characterEditorGetLevelsPerPerk();
static void characterEditorGrantPerkAtLevel(int level);
static int _is_supper_bonus();
static int characterEditorFolderViewInit();
static void characterEditorFolderViewScroll(int direction);
@@ -777,7 +769,7 @@ static int gCharacterEditorTaggedSkillCount;
static int gCharacterEditorTempTaggedSkills[NUM_TAGGED_SKILLS];
// 0x570A28
static unsigned char gCharacterEditorHasFreePerkBackup;
static char gCharacterEditorHasFreePerkBackup;
// 0x570A29
static unsigned char gCharacterEditorHasFreePerk;
@@ -2482,7 +2474,7 @@ static void characterEditorDrawPrimaryStat(int stat, bool animate, int previousV
characterEditorDrawBigNumber(58, gCharacterEditorPrimaryStatY[stat], flags, value, previousValue, gCharacterEditorWindow);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, kPrimaryStatDescriptionWidth, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, 40, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
messageListItemId = critterGetStat(gDude, stat) + 199;
if (messageListItemId > 210) {
@@ -2494,7 +2486,7 @@ static void characterEditorDrawPrimaryStat(int stat, bool animate, int previousV
} else {
value = critterGetStat(gDude, stat);
characterEditorDrawBigNumber(58, gCharacterEditorPrimaryStatY[stat], 0, value, 0, gCharacterEditorWindow);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, kPrimaryStatDescriptionWidth, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, 40, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
value = critterGetStat(gDude, stat);
if (value > 10) {
@@ -5676,117 +5668,11 @@ int characterEditorLoad(File* stream)
return 0;
}
int characterEditorGetPerkOwed()
{
return gCharacterEditorHasFreePerk;
}
void characterEditorSetPerkOwed(int value)
{
unsigned int maskedValue = static_cast<unsigned int>(value) & 0xFF;
if (maskedValue <= kMaxPerkOwed) {
gCharacterEditorHasFreePerk = maskedValue;
}
}
// External interface: called when level up occurs
void characterEditorHandleLevelUp(int level)
{
characterEditorGrantPerkAtLevel(level);
}
// compute the character level that the next perk selection should use
int characterEditorGetPerkSelectionLevel()
{
int currentLevel = pcGetStat(PC_STAT_LEVEL);
if (settings.gameplay.perk_carryover != PERK_CARRY_OVER_MODE_ON) {
return currentLevel;
}
int owed = characterEditorGetPerkOwed();
if (owed <= 1) {
// If a single perk is owed, use the current level. This prevents odd effects in
// saves where perks have been skipped and is a general safe fallback.
return currentLevel;
}
int levelsPerPerk = characterEditorGetLevelsPerPerk();
// This therefore uses the number of perk thresholds strictly before the
// current level, then compares that against the total perk ranks already
// spent. In the common non-scripted case:
// - level 6 with 0 spent perks yields one deferred threshold (level 3)
// - after spending one perk, there is no older backlog left, so the next
// owed perk uses level 6 requirements
// - level 9 with 2 spent perks has no older backlog, so owed perks are
// immediately spendable at level 9
int previousThresholdsReached = (currentLevel - 1) / levelsPerPerk;
int spentPerks = 0;
for (int perk = 0; perk < PERK_COUNT; perk++) {
spentPerks += perkGetRank(gDude, perk);
}
// This is a heuristic for the 99.9% case where perk ranks come from normal
// perk picks. If enough perk ranks have already been spent to cover all
// earlier thresholds, there is no deferred backlog to force in order.
if (spentPerks >= previousThresholdsReached) {
return currentLevel;
}
// The next deferred pick corresponds to the earliest previously missed
// threshold after the already-spent perk ranks.
int thresholdIndex = spentPerks + 1;
int selectionLevel = std::max(3, thresholdIndex * levelsPerPerk);
if (selectionLevel <= 0 || selectionLevel > currentLevel) {
return currentLevel;
}
return selectionLevel;
}
// 0x43C20C
void characterEditorReset()
{
gCharacterEditorRemainingCharacterPoints = 5;
gCharacterEditorLastLevel = 1;
characterEditorSetPerkOwed(0);
}
static int characterEditorGetLevelsPerPerk()
{
int progression = 3;
if (traitIsSelected(TRAIT_SKILLED)) {
progression += 1;
}
return progression;
}
static void characterEditorGrantPerkAtLevel(int level)
{
if (level <= 0 || level % characterEditorGetLevelsPerPerk() != 0) {
return;
}
int selectedPerksCount = 0;
for (int perk = 0; perk < PERK_COUNT; perk++) {
if (perkGetRank(gDude, perk) != 0) {
selectedPerksCount += 1;
if (selectedPerksCount >= kMaxSelectablePerks) {
return;
}
}
}
switch (settings.gameplay.perk_carryover) {
case PERK_CARRY_OVER_MODE_OFF:
characterEditorSetPerkOwed(1);
break;
case PERK_CARRY_OVER_MODE_ON:
case PERK_CARRY_OVER_MODE_SFALL:
characterEditorSetPerkOwed(characterEditorGetPerkOwed() + 1);
break;
}
}
// level up if needed
@@ -5796,13 +5682,6 @@ static int characterEditorUpdateLevel()
{
int level = pcGetStat(PC_STAT_LEVEL);
if (level != gCharacterEditorLastLevel && level <= PC_LEVEL_MAX) {
// Runtime level-up now grants perks immediately. However, older saves
// may still have last_level behind the real level with no owed perks
// recorded, because perk grants used to be deferred until the next
// character screen visit. Replay perk grants only for that legacy
// catch-up case.
bool shouldReplayPerkGrants = characterEditorGetPerkOwed() == 0;
for (int nextLevel = gCharacterEditorLastLevel + 1; nextLevel <= level; nextLevel++) {
int sp = pcGetStat(PC_STAT_UNSPENT_SKILL_POINTS);
sp += 5;
@@ -5821,13 +5700,30 @@ static int characterEditorUpdateLevel()
pcSetStat(PC_STAT_UNSPENT_SKILL_POINTS, sp);
if (shouldReplayPerkGrants) {
characterEditorGrantPerkAtLevel(nextLevel);
int selectedPerksCount = 0;
for (int perk = 0; perk < PERK_COUNT; perk++) {
if (perkGetRank(gDude, perk) != 0) {
selectedPerksCount += 1;
if (selectedPerksCount >= 37) {
break;
}
}
}
if (selectedPerksCount < 37) {
int progression = 3;
if (traitIsSelected(TRAIT_SKILLED)) {
progression += 1;
}
if (nextLevel % progression == 0) {
gCharacterEditorHasFreePerk = 1;
}
}
}
}
while (characterEditorGetPerkOwed() != 0) {
if (gCharacterEditorHasFreePerk != 0) {
characterEditorWindowSelectedFolder = 0;
characterEditorDrawFolders();
windowRefresh(gCharacterEditorWindow);
@@ -5836,15 +5732,11 @@ static int characterEditorUpdateLevel()
if (rc == -1) {
debugPrint("\n *** Error running perks dialog! ***\n");
return -1;
}
if (rc == 0) {
} else if (rc == 0) {
characterEditorDrawFolders();
break;
}
if (rc == 1) {
} else if (rc == 1) {
characterEditorDrawFolders();
gCharacterEditorHasFreePerk = 0;
}
}
@@ -5893,10 +5785,6 @@ static int perkDialogShow()
gPerkDialogCardFrmId = -1;
gPerkDialogCardTitle[0] = '\0';
gPerkDialogCardDrawn = false;
int previousPerkRanks[PERK_COUNT];
for (int perk = 0; perk < PERK_COUNT; perk++) {
previousPerkRanks[perk] = perkGetRank(gDude, perk);
}
int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 86, 0, 0, 0);
if (!_perkDialogBackgroundFrmImage.lock(backgroundFid)) {
@@ -6022,12 +5910,6 @@ static int perkDialogShow()
fontDrawText(gPerkDialogWindowBuffer + PERK_WINDOW_WIDTH * 186 + 171, msg, PERK_WINDOW_WIDTH, PERK_WINDOW_WIDTH, _colorTable[18979]);
int count = perkDialogDrawPerks();
if (count == 0) {
debugPrint("*** No perks available for current owed slot! ***");
_perkDialogBackgroundFrmImage.unlock();
windowDestroy(gPerkDialogWindow);
return -1;
}
// NOTE: Original code is slightly different, but does the same thing.
int perk = gPerkDialogOptionList[gPerkDialogTopLine + gPerkDialogCurrentLine].value;
@@ -6049,8 +5931,7 @@ static int perkDialogShow()
int rc = perkDialogHandleInput(count, perkDialogRefreshPerks);
if (rc == 1) {
int selectionLevel = characterEditorGetPerkSelectionLevel();
if (perkAddAtLevel(gDude, gPerkDialogOptionList[gPerkDialogTopLine + gPerkDialogCurrentLine].value, selectionLevel) == -1) {
if (perkAdd(gDude, gPerkDialogOptionList[gPerkDialogTopLine + gPerkDialogCurrentLine].value) == -1) {
debugPrint("\n*** Unable to add perk! ***\n");
rc = 2;
}
@@ -6059,20 +5940,19 @@ static int perkDialogShow()
rc &= 1;
if (rc != 0) {
characterEditorSetPerkOwed(characterEditorGetPerkOwed() - 1);
if (perkGetRank(gDude, PERK_TAG) != 0 && previousPerkRanks[PERK_TAG] == 0) {
if (perkGetRank(gDude, PERK_TAG) != 0 && gCharacterEditorPerksBackup[PERK_TAG] == 0) {
if (!perkDialogHandleTagPerk()) {
perkRemove(gDude, PERK_TAG);
}
} else if (perkGetRank(gDude, PERK_MUTATE) != 0 && previousPerkRanks[PERK_MUTATE] == 0) {
} else if (perkGetRank(gDude, PERK_MUTATE) != 0 && gCharacterEditorPerksBackup[PERK_MUTATE] == 0) {
if (!perkDialogHandleMutatePerk()) {
perkRemove(gDude, PERK_MUTATE);
}
} else if (perkGetRank(gDude, PERK_LIFEGIVER) != previousPerkRanks[PERK_LIFEGIVER]) {
} else if (perkGetRank(gDude, PERK_LIFEGIVER) != gCharacterEditorPerksBackup[PERK_LIFEGIVER]) {
int maxHp = critterGetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS);
critterSetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS, maxHp + 4);
critterAdjustHitPoints(gDude, 4);
} else if (perkGetRank(gDude, PERK_EDUCATED) != previousPerkRanks[PERK_EDUCATED]) {
} else if (perkGetRank(gDude, PERK_EDUCATED) != gCharacterEditorPerksBackup[PERK_EDUCATED]) {
int sp = pcGetStat(PC_STAT_UNSPENT_SKILL_POINTS);
pcSetStat(PC_STAT_UNSPENT_SKILL_POINTS, sp + 2);
}
@@ -6386,7 +6266,7 @@ static int perkDialogDrawPerks()
fontSetCurrent(101);
int perks[PERK_COUNT];
int count = perkGetAvailablePerks(gDude, characterEditorGetPerkSelectionLevel(), perks);
int count = perkGetAvailablePerks(gDude, perks);
if (count == 0) {
return 0;
}
-14
View File
@@ -7,16 +7,6 @@ namespace fallout {
extern int gCharacterEditorRemainingCharacterPoints;
enum PerkCarryOverMode {
// Unspent perks are lost on level up (vanilla)
PERK_CARRY_OVER_MODE_OFF = 0,
// Unspent perks are preserved on level up, but you still spend unspent perks at the appropriate levels.
// E.g. if you reach level 9 and have never spent any perks, you first choose from the level 3 perks, then 6, then 9. (CE)
PERK_CARRY_OVER_MODE_ON = 1,
// Unspent perks are preserved on level up, and you can choose to spend them on perks for the current level.
PERK_CARRY_OVER_MODE_SFALL = 2,
};
int characterEditorShow(bool isCreationMode);
void characterEditorInit();
bool _isdoschar(int ch);
@@ -24,10 +14,6 @@ char* _strmfe(char* dest, const char* name, const char* ext);
int characterEditorSave(File* stream);
int characterEditorLoad(File* stream);
void characterEditorReset();
int characterEditorGetPerkOwed();
void characterEditorSetPerkOwed(int value);
void characterEditorHandleLevelUp(int level);
int characterEditorGetPerkSelectionLevel();
} // namespace fallout
+2 -2
View File
@@ -4938,6 +4938,7 @@ static void _damage_object(Object* a1, int damage, bool animated, int a4, Object
}
partyMemberRemove(a1);
scriptHooks_OnDeath(a1);
}
}
@@ -6705,8 +6706,7 @@ static void damageModInit()
gBonusHthDamageFix = true;
configGetBool(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_BONUS_HTH_DAMAGE_FIX_KEY, &gBonusHthDamageFix);
gDisplayBonusDamage = false;
configGetBool(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_DISPLAY_BONUS_DAMAGE_KEY, &gDisplayBonusDamage);
gDisplayBonusDamage = settings.ui.display_bonus_damage;
}
bool damageModGetBonusHthDamageFix()
+12 -6
View File
@@ -29,6 +29,7 @@
#include "random.h"
#include "scripts.h"
#include "settings.h"
#include "sfall_script_hooks.h"
#include "skill.h"
#include "stat.h"
#include "svga.h"
@@ -1692,7 +1693,7 @@ static Object* _ai_danger_source(Object* a1)
for (int index = 0; index < 4; index++) {
Object* candidate = targets[index];
if (candidate != nullptr && isWithinPerception(a1, candidate)) {
if (candidate != nullptr && isWithinPerceptionDetailed(a1, candidate, PERCEPTION_AI_TARGET) != PERCEPTION_OUT_OF_RANGE) {
if (pathfinderFindPath(a1, a1->tile, candidate->tile, nullptr, 0, _obj_blocking_at) != 0
|| _combat_check_bad_shot(a1, candidate, HIT_MODE_RIGHT_WEAPON_PRIMARY, false) == COMBAT_BAD_SHOT_OK) {
return candidate;
@@ -3510,10 +3511,10 @@ void _combatai_check_retaliation(Object* a1, Object* a2)
}
// 0x42BA04
bool isWithinPerception(Object* critter, Object* target)
PerceptionResult isWithinPerceptionDetailed(Object* critter, Object* target, PerceptionType type)
{
if (target == nullptr) {
return false;
return PERCEPTION_OUT_OF_RANGE;
}
int distance = objectGetDistanceBetween(target, critter);
@@ -3537,7 +3538,7 @@ bool isWithinPerception(Object* critter, Object* target)
}
if (distance <= maxDistance) {
return true;
return scriptHooks_WithinPerception(critter, target, type, PERCEPTION_IN_RANGE);
}
}
@@ -3560,10 +3561,15 @@ bool isWithinPerception(Object* critter, Object* target)
}
if (distance <= maxDistance) {
return true;
return scriptHooks_WithinPerception(critter, target, type, PERCEPTION_IN_RANGE);
}
return false;
return scriptHooks_WithinPerception(critter, target, type, PERCEPTION_OUT_OF_RANGE);
}
bool isWithinPerception(Object* watcher, Object* target)
{
return isWithinPerceptionDetailed(watcher, target) != PERCEPTION_OUT_OF_RANGE;
}
// Load combatai.msg and apply language filter.
+5 -1
View File
@@ -5,6 +5,7 @@
#include "combat_defs.h"
#include "db.h"
#include "obj_types.h"
#include "sfall_script_hooks.h"
namespace fallout {
@@ -64,7 +65,10 @@ int critterSetAiPacket(Object* object, int aiPacket);
int _combatai_msg(Object* critter, Attack* attack, int type, int delay);
Object* _combat_ai_random_target(Attack* attack);
void _combatai_check_retaliation(Object* a1, Object* a2);
bool isWithinPerception(Object* a1, Object* a2);
// Use this when the caller needs hook-specific context or must distinguish
// PERCEPTION_FORCE from the normal in-range/out-of-range result.
PerceptionResult isWithinPerceptionDetailed(Object* critter, Object* target, PerceptionType type = PERCEPTION_OTHER);
bool isWithinPerception(Object* watcher, Object* target);
void aiMessageListReloadIfNeeded();
void _combatai_notify_onlookers(Object* a1);
void _combatai_notify_friends(Object* a1);
+286 -40
View File
@@ -7,6 +7,10 @@
#include <stdlib.h>
#include <string.h>
#include <set>
#include <string>
#include <vector>
#include "db.h"
#include "memory.h"
#include "platform_compat.h"
@@ -18,11 +22,25 @@ namespace fallout {
// The initial number of sections (or key-value) pairs in the config.
#define CONFIG_INITIAL_CAPACITY (10)
struct CaseInsensitiveLess {
bool operator()(const std::string& a, const std::string& b) const
{
return compat_stricmp(a.c_str(), b.c_str()) < 0;
}
};
typedef std::set<std::string, CaseInsensitiveLess> StringSet;
static bool configParseLine(Config* config, char* string);
static bool configParseKeyValue(char* string, char* key, char* value);
static bool configEnsureSectionExists(Config* config, const char* sectionKey);
static bool configTrimString(char* string);
static bool configWriteDb(Config* config, const char* filePath);
static bool configWriteStandard(Config* config, const char* filePath);
static bool configWriteSideBySide(Config* config, const char* filePath, int flags);
static void configWriteSection(FILE* stream, const char* sectionName, ConfigSection* section, const StringSet* handledKeys);
// Last section key read from .INI file.
//
// 0x518224
@@ -333,53 +351,277 @@ bool configRead(Config* config, const char* filePath, bool isDb)
//
// 0x42C324
bool configWrite(Config* config, const char* filePath, bool isDb)
{
return configWriteEx(config, filePath, isDb ? CONFIG_IS_DB : CONFIG_DEFAULT);
}
bool configWriteEx(Config* config, const char* filePath, int flags)
{
if (config == nullptr || filePath == nullptr) {
return false;
}
if (isDb) {
File* stream = fileOpen(filePath, "wt");
if (stream == nullptr) {
return false;
}
for (int sectionIndex = 0; sectionIndex < config->entriesLength; sectionIndex++) {
DictionaryEntry* sectionEntry = &(config->entries[sectionIndex]);
filePrintFormatted(stream, "[%s]\n", sectionEntry->key);
ConfigSection* section = (ConfigSection*)sectionEntry->value;
for (int index = 0; index < section->entriesLength; index++) {
DictionaryEntry* keyValueEntry = &(section->entries[index]);
filePrintFormatted(stream, "%s=%s\n", keyValueEntry->key, *(char**)keyValueEntry->value);
}
filePrintFormatted(stream, "\n");
}
fileClose(stream);
} else {
FILE* stream = compat_fopen(filePath, "wt");
if (stream == nullptr) {
return false;
}
for (int sectionIndex = 0; sectionIndex < config->entriesLength; sectionIndex++) {
DictionaryEntry* sectionEntry = &(config->entries[sectionIndex]);
fprintf(stream, "[%s]\n", sectionEntry->key);
ConfigSection* section = (ConfigSection*)sectionEntry->value;
for (int index = 0; index < section->entriesLength; index++) {
DictionaryEntry* keyValueEntry = &(section->entries[index]);
fprintf(stream, "%s=%s\n", keyValueEntry->key, *(char**)keyValueEntry->value);
}
fprintf(stream, "\n");
}
fclose(stream);
if (flags & CONFIG_IS_DB) {
return configWriteDb(config, filePath);
}
if (flags & (CONFIG_RETAIN_COMMENTS | CONFIG_RETAIN_ORDER | CONFIG_RETAIN_UNKNOWN)) {
return configWriteSideBySide(config, filePath, flags);
}
return configWriteStandard(config, filePath);
}
static bool configWriteDb(Config* config, const char* filePath)
{
File* stream = fileOpen(filePath, "wt");
if (stream == nullptr) {
return false;
}
for (int i = 0; i < config->entriesLength; i++) {
DictionaryEntry* sectionEntry = &(config->entries[i]);
filePrintFormatted(stream, "[%s]\n", sectionEntry->key);
ConfigSection* section = (ConfigSection*)sectionEntry->value;
for (int j = 0; j < section->entriesLength; j++) {
DictionaryEntry* keyValueEntry = &(section->entries[j]);
filePrintFormatted(stream, "%s=%s\n", keyValueEntry->key, *(char**)keyValueEntry->value);
}
filePrintFormatted(stream, "\n");
}
fileClose(stream);
return true;
}
static bool configWriteStandard(Config* config, const char* filePath)
{
FILE* stream = compat_fopen(filePath, "wt");
if (stream == nullptr) {
return false;
}
for (int i = 0; i < config->entriesLength; i++) {
configWriteSection(stream, config->entries[i].key, (ConfigSection*)config->entries[i].value, nullptr);
fprintf(stream, "\n");
}
fclose(stream);
return true;
}
static void configWriteSection(FILE* stream, const char* sectionName, ConfigSection* section, const StringSet* handledKeys)
{
if (sectionName != nullptr) {
fprintf(stream, "[%s]\n", sectionName);
}
for (int i = 0; i < section->entriesLength; i++) {
DictionaryEntry* entry = &(section->entries[i]);
if (handledKeys == nullptr || handledKeys->find(entry->key) == handledKeys->end()) {
fprintf(stream, "%s=%s\n", entry->key, *(char**)entry->value);
}
}
}
static bool configWriteSideBySide(Config* config, const char* filePath, int flags)
{
bool retainUnknown = (flags & CONFIG_RETAIN_UNKNOWN) != 0;
FILE* original = compat_fopen(filePath, "rt");
if (original == nullptr) {
if (errno == ENOENT) {
// File doesn't exist, nothing to retain, perform standard write.
return configWriteStandard(config, filePath);
}
// Other error (e.g., permissions), fail as requested.
return false;
}
char tempPath[COMPAT_MAX_PATH];
int tempPathLength = snprintf(tempPath, sizeof(tempPath), "%s.tmp", filePath);
if (tempPathLength < 0 || tempPathLength >= (int)sizeof(tempPath)) {
fclose(original);
return false;
}
FILE* output = compat_fopen(tempPath, "wt");
if (output == nullptr) {
fclose(original);
return false;
}
StringSet handledSections;
StringSet handledKeys;
std::vector<std::string> pendingLines;
char currentSectionName[CONFIG_FILE_MAX_LINE_LENGTH] = "";
ConfigSection* currentSection = nullptr;
// Treat pre-section preamble as "known" so its comments are preserved.
bool currentSectionKnown = true;
char line[CONFIG_FILE_MAX_LINE_LENGTH];
while (compat_fgets(line, sizeof(line), original) != nullptr) {
char lineCopy[CONFIG_FILE_MAX_LINE_LENGTH];
strcpy(lineCopy, line);
char* trimmed = lineCopy;
while (isspace(static_cast<unsigned char>(*trimmed))) {
trimmed++;
}
// Buffer comments and empty lines; drop them if inside a suppressed section.
if (*trimmed == '\0' || *trimmed == ';' || *trimmed == '#') {
if (!currentSectionKnown && !retainUnknown) {
continue;
}
if ((flags & CONFIG_RETAIN_COMMENTS) || (*trimmed == '\0')) {
pendingLines.emplace_back(line);
}
continue;
}
if (*trimmed == '[') {
// End current section, write out remaining keys from it.
if (currentSection != nullptr) {
configWriteSection(output, nullptr, currentSection, &handledKeys);
handledKeys.clear();
currentSection = nullptr;
}
// Parse the section name before deciding whether to write.
currentSectionKnown = false;
currentSectionName[0] = '\0';
char* end = strchr(trimmed, ']');
if (end != nullptr) {
*end = '\0';
strcpy(currentSectionName, trimmed + 1);
configTrimString(currentSectionName);
*end = ']';
int sectionIndex = dictionaryGetIndexByKey(config, currentSectionName);
if (sectionIndex != -1) {
currentSection = (ConfigSection*)config->entries[sectionIndex].value;
handledSections.insert(currentSectionName);
currentSectionKnown = true;
}
}
if (currentSectionKnown || retainUnknown) {
// Flush buffered comments/empty lines that preceded this header.
for (const std::string& l : pendingLines) {
fprintf(output, "%s", l.c_str());
}
pendingLines.clear();
fprintf(output, "%s", line);
} else {
// Suppress unknown section entirely, discard buffered lines.
pendingLines.clear();
}
continue;
}
char* equals = strchr(trimmed, '=');
if (equals != nullptr) {
*equals = '\0';
char key[CONFIG_FILE_MAX_LINE_LENGTH];
strcpy(key, trimmed);
configTrimString(key);
*equals = '=';
char* value = nullptr;
if (currentSection != nullptr) {
configGetString(config, currentSectionName, key, &value);
}
if (value != nullptr) {
// Flush lines that were between previous key and this one.
for (const std::string& l : pendingLines) {
fprintf(output, "%s", l.c_str());
}
pendingLines.clear();
const char* k = key;
char* comment = nullptr;
if (flags & CONFIG_RETAIN_COMMENTS) {
*equals = '\0';
k = trimmed;
if ((comment = strchr(equals + 1, ';')) == nullptr) {
comment = strchr(equals + 1, '#');
}
}
fprintf(output, "%s=%s%s%s", k, value, comment ? " " : "", comment ? comment : "\n");
handledKeys.insert(key);
} else if (retainUnknown) {
// Key not in config - write verbatim from original file.
for (const std::string& l : pendingLines) {
fprintf(output, "%s", l.c_str());
}
pendingLines.clear();
fprintf(output, "%s", line);
}
// else: drop key; keep pending lines buffered so they attach to the
// next surviving key rather than becoming orphaned.
continue;
}
// Unknown line structure - buffer if retaining comments/structure.
if ((currentSectionKnown || retainUnknown) && (flags & CONFIG_RETAIN_COMMENTS)) {
pendingLines.emplace_back(line);
}
}
// Write remaining keys in the last processed section.
if (currentSection != nullptr) {
configWriteSection(output, nullptr, currentSection, &handledKeys);
}
// Flush remaining comments/empty lines.
for (const std::string& l : pendingLines) {
fprintf(output, "%s", l.c_str());
}
pendingLines.clear();
// Write completely new sections, separated from existing content.
bool firstNewSection = true;
for (int i = 0; i < config->entriesLength; i++) {
DictionaryEntry* sectionEntry = &(config->entries[i]);
if (handledSections.find(sectionEntry->key) == handledSections.end()) {
if (firstNewSection) {
fprintf(output, "\n");
firstNewSection = false;
}
configWriteSection(output, sectionEntry->key, (ConfigSection*)sectionEntry->value, nullptr);
fprintf(output, "\n");
}
}
fclose(output);
fclose(original);
std::string backupPath = std::string(filePath) + ".bak";
if (compat_remove(backupPath.c_str()) != 0 && errno != ENOENT) {
compat_remove(tempPath);
return false;
}
if (compat_rename(filePath, backupPath.c_str()) != 0) {
compat_remove(tempPath);
return false;
}
if (compat_rename(tempPath, filePath) != 0) {
compat_rename(backupPath.c_str(), filePath);
return false;
}
if (compat_remove(backupPath.c_str()) != 0 && errno != ENOENT) {
return false;
}
return true;
}
@@ -403,6 +645,10 @@ static bool configParseLine(Config* config, char* string)
// Find comment marker and truncate the string.
pch = strchr(string, ';');
if (pch == nullptr) {
pch = strchr(string, '#');
}
if (pch != nullptr) {
*pch = '\0';
}
+15
View File
@@ -5,6 +5,20 @@
namespace fallout {
enum ConfigFlags {
CONFIG_DEFAULT = 0,
// Config will be read from DB (VFS)
CONFIG_IS_DB = (1 << 0),
// Keep existing comments when overwriting file. Also retains order.
CONFIG_RETAIN_COMMENTS = (1 << 1),
// Keep existing order of keys when overwriting file.
CONFIG_RETAIN_ORDER = (1 << 2),
// Keep sections and keys that are not in the saved config data when overwriting. Also retains order.
CONFIG_RETAIN_UNKNOWN = (1 << 3),
// Keep existing lines as much as possible when overwriting file, only add/update values.
CONFIG_RETAIN_ALL = CONFIG_RETAIN_COMMENTS | CONFIG_RETAIN_ORDER | CONFIG_RETAIN_UNKNOWN,
};
// A representation of .INI file.
//
// It's implemented as a [Dictionary] whos keys are section names of .INI file,
@@ -30,6 +44,7 @@ bool configGetIntList(Config* config, const char* section, const char* key, int*
bool configSetInt(Config* config, const char* sectionKey, const char* key, int value);
bool configRead(Config* config, const char* filePath, bool isDb);
bool configWrite(Config* config, const char* filePath, bool isDb);
bool configWriteEx(Config* config, const char* filePath, int flags);
bool configGetDouble(Config* config, const char* sectionKey, const char* key, double* valuePtr);
bool configSetDouble(Config* config, const char* sectionKey, const char* key, double value);
+5 -3
View File
@@ -25,6 +25,7 @@
#include "random.h"
#include "reaction.h"
#include "scripts.h"
#include "sfall_script_hooks.h"
#include "skill.h"
#include "stat.h"
#include "tile.h"
@@ -825,7 +826,11 @@ void critterKill(Object* critter, int anim, bool refreshRect)
int elevation = critter->elevation;
critter->data.critter.hp = 0;
critter->data.critter.combat.results |= DAM_DEAD;
partyMemberRemove(critter);
scriptHooks_OnDeath(critter);
// NOTE: Original code uses goto to jump out from nested conditions below.
bool shouldChangeFid = false;
@@ -893,9 +898,6 @@ void critterKill(Object* critter, int anim, bool refreshRect)
_obj_turn_off_light(critter, &tempRect);
rectUnion(&updatedRect, &tempRect, &updatedRect);
critter->data.critter.hp = 0;
critter->data.critter.combat.results |= DAM_DEAD;
if (critter->sid != -1) {
scriptRemove(critter->sid);
critter->sid = -1;

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