Compare commits

..
Author SHA1 Message Date
Mike Klaas 1b37a9b2dc Fix f2_res.ini migration
In https://github.com/fallout2-ce/fallout2-ce/commit/9e8eaa13f56988ef9e462d0ec6a31833931b7297, f2_res.ini stopped getting migrated, since settings were loaded in memory (with defaults) earlier.  This is a small fix that checks the value in the file instead of in memory.

Also, added some correctness checking to global scripts loading.  somehow I had a length 0 .int file in my scripts/ directly which caused a crash (now skips with an error log)
2026-04-22 23:20:56 -07:00
Vlad KandCopilot efd8a4a525 Fix last remaining warnings in MSVC (#406)
* Fix last remaining warnings in MSVC

* Update CMakeLists.txt

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 21:04:21 +00:00
Vlad Kandgithub-actions[bot] 4c09ffe9e6 Extended AP bar from sfall (#400)
* Extended AP bar from sfall

* Rewrote using new Buffer2D and blitBuffer2D + code review fixes

* chore: auto-format with clang-format

* Add ConstBuffer2D + other fixes

* chore: auto-format with clang-format

* Linux build fix attempt

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-22 22:09:12 +02:00
Mike Klaas 1026c79ac5 Implement objects_in_radius (#399)
* Implement `objects_in_radius`

Used by Et tu and a few other mods
2026-04-21 22:20:59 -07:00
Vlad K 3849b4ea17 Add AI agent related files to git ignore (#395)
* Add AI agent related files to git ignore

* Add gemini.md
2026-04-21 09:46:59 +02:00
Mike Klaas 6cbe68aef9 Fix setup (play) instructions, and add basic compile instructions to CONTRIBUTING.md (#390)
* Fix setup (play) instructions, and add basic compile instructions to CONTRIBUTING.md
2026-04-21 06:09:42 +00:00
Mike Klaas 066595f683 Add Fallout 1 .dat file support to fallout2-dat (#393) 2026-04-20 20:21:19 -07:00
33 changed files with 1508 additions and 297 deletions
+12
View File
@@ -400,3 +400,15 @@ FodyWeavers.xsd
# versioning header
src/platform/git_version.h
CMakeUserPresets.json
# Local configs for AI agents
.opencode
.agents
.aider*
.gemini
.claude
.mcp.json
AGENTS.md
CLAUDE.md
GEMINI.md
opencode.json
+30 -19
View File
@@ -20,6 +20,10 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
if(MSVC)
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:/EHsc>)
endif()
option(FALLOUT_VENDORED "Use vendored third-party libraries" ON)
if(ANDROID)
@@ -448,27 +452,34 @@ 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"
if((NOT ANDROID) AND (NOT IOS) AND (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten"))
add_executable(fallout2-dat
"tools/dat_tool.cc"
"tools/dat_archive.cc"
"tools/fo1_dat_lzss.cc"
"src/dfile.cc"
"src/platform_compat.cc"
)
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)
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}
)
if(WIN32)
target_link_libraries(fallout2-dat winmm)
endif()
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})
endif()
if(APPLE)
if(IOS)
+51 -5
View File
@@ -2,13 +2,59 @@
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.
For current sfall compatibility status and the remaining work needed to close gaps, see [SFALL_COMPATIBILITY.md](SFALL_COMPATIBILITY.md).
## Building
The project uses CMake, so building mostly comes down to picking the right preset.
### macOS
- Install the Command Line Tools if needed: `xcode-select --install`
- Install the common build dependencies: `brew install cmake ninja`
- Configure and build:
```sh
cmake -S . -B out/build/macos -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build out/build/macos --target fallout2-ce
```
### Linux
Install SDL and a toolchain:
- Debian / Ubuntu:
```sh
sudo apt install build-essential cmake ninja-build libsdl2-dev zlib1g-dev
```
- Fedora:
```sh
sudo dnf install gcc-c++ cmake ninja-build SDL2-devel zlib-devel
```
- Arch Linux:
```sh
sudo pacman -S base-devel cmake ninja sdl2 zlib
```
Then build:
```
cmake -S . -B out/build/linux-x64-debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build out/build/linux-x64-debug --target fallout2-ce
```
### Windows
TODO: add Visual Studio build instructions. This will likely need a Visual Studio generator or preset plus the MSVC toolchain and Windows SDK.
## `.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.
There is a small command-line archive tool in this repo for inspecting Fallout `.dat` files. It supports both Fallout 2 and Fallout 1 archives.
Build it with `make TARGET=fallout2-dat`.
From the repo root, build it with your normal CMake workflow, targeting `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.
`cmake --build out/build/<preset-name> --target fallout2-dat`
The executable is written to your selected build directory, so the exact path will vary by preset and configuration.
The current tool is read-only. Available commands:
@@ -18,9 +64,9 @@ The current tool is read-only. Available commands:
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:
For example:
`/Users/klaas/game/fallout2-ce/out/build/local-debug-arm64/fallout2-dat master.dat extract --lower /tmp/fallout2-dat-lower data\\*`
`fallout2-dat master.dat extract --lower /tmp/fallout2-dat-lower data\\*`
## Updating SDL
+12 -19
View File
@@ -2,13 +2,15 @@
Fallout 2 Community Edition is a fully working re-implementation of Fallout 2, with the same original gameplay, engine bugfixes, and some quality of life improvements, that works (mostly) hassle-free on multiple platforms. This is a fork of the original project, which isn't getting regular updates.
Popular Fallout 2 total conversion mods are partially supported. Original versions of Nevada and Sonora (that do not rely on extended features provided by Sfall) work. [Fallout 2 Restoration Project](https://github.com/BGforgeNet/Fallout2_Restoration_Project), [Fallout Et Tu](https://github.com/rotators/Fo1in2) and [Olympus 2207](https://olympus2207.com) are not yet supported. Other mods (particularly Resurrection and Yesterday) are not tested.
Popular Fallout 2 total conversion mods are partially supported. Original versions of Nevada and Sonora (that do not rely on extended features provided by Sfall) work. [Fallout 2 Restoration Project](https://github.com/BGforgeNet/Fallout2_Restoration_Project) is supported (in Beta). [Fallout Et Tu](https://github.com/rotators/Fo1in2) and [Olympus 2207](https://olympus2207.com) are not yet supported. Other mods (particularly Resurrection and Yesterday) are not tested.
There is also [Fallout Community Edition](https://github.com/alexbatalov/fallout1-ce) (not affiliated with this fork).
For build and contributor notes, see [CONTRIBUTING.md](CONTRIBUTING.md).
## Installation
You must own the game to play. Purchase your copy on [GOG](https://www.gog.com/game/fallout_2), [Epic Games](https://store.epicgames.com/p/fallout-2) or [Steam](https://store.steampowered.com/app/38410). Download latest [release](https://github.com/fallout2-ce/fallout2-ce/releases) or build from source.
You must own the game to play. Purchase your copy on [GOG](https://www.gog.com/game/fallout_2), [Epic Games](https://store.epicgames.com/p/fallout-2) or [Steam](https://store.steampowered.com/app/38410). Download latest [release](https://github.com/fallout2-ce/fallout2-ce/releases) or build from source.
### Windows
@@ -22,17 +24,10 @@ Download and copy `fallout2-ce.exe` to your `Fallout2` folder. It serves as a dr
```console
$ sudo apt install innoextract
$ innoextract ~/Downloads/setup_fallout2_2.1.0.18.exe -I app
$ mv app Fallout2
$ innoextract ~/Downloads/setup_fallout_2_1.02_gog_v1_\(77792\).exe -d Fallout2
```
- Download and copy `fallout2-ce` to this folder.
- Install [SDL2](https://libsdl.org/download-2.0.php):
```console
$ sudo apt install libsdl2-2.0-0
```
- Download the Linux release archive, extract `fallout2-ce`, and copy it into this folder.
- Run `./fallout2-ce`.
@@ -48,13 +43,13 @@ $ sudo apt install libsdl2-2.0-0
```console
$ brew install innoextract
$ innoextract ~/Downloads/setup_fallout2_2.1.0.18.exe -I app
$ mv app /Applications/Fallout2
$ innoextract ~/Downloads/setup_fallout_2_1.02_gog_v1_\(77792\).exe -d fallout2
$ mv fallout2 /Applications/Fallout2
```
- Download and copy `fallout2-ce.app` to this folder.
- [Download](https://github.com/fallout2-ce/fallout2-ce/releases) and copy `Fallout II Community Edition.app` to this folder.
- Run `fallout2-ce.app`.
- Run `Fallout II Community Edition.app`.
### Android
@@ -141,11 +136,9 @@ For a sample ddraw.ini configuration file, containing all currently working sett
## Contributing
Integrating Sfall goodies is the top priority. Quality of life updates are OK too. Please no large scale refactorings at this time as we need to reconcile changes from Reference Edition, which will make this process slow and error-prone. In any case open up an issue with your suggestion or to notify other people that something is being worked on.
Integrating Sfall goodies is the top priority. Quality of life updates are OK too. In any case open up an issue with your suggestion or to notify other people that something is being worked on.
### Integrating Sfall
There are literally hundreds if not thousands of fixes and features in sfall. I guess not all of them are needed in Community Edition, but for the sake of compatibility with big mods out there, let's integrate them all.
For current sfall compatibility status and the remaining work needed to close gaps, see [SFALL_COMPATIBILITY.md](SFALL_COMPATIBILITY.md).
## License
+3 -3
View File
@@ -52,7 +52,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| Explosions | set_attack_explosion_pattern<br>set_attack_explosion_art<br>set_attack_explosion_radius<br>set_attack_is_explosion_fire<br>set_explosion_radius<br>set_dynamite_damage<br>set_plastic_damage<br>get_explosion_damage<br>set_explosion_max_targets<br>item_make_explosive | âś… except item_make_explosive | - |
| Animations | reg_anim_combat_check<br>reg_anim_destroy<br>reg_anim_animate_and_hide<br>reg_anim_light<br>reg_anim_change_fid<br>reg_anim_take_out<br>reg_anim_turn_towards<br>reg_anim_callback<br>reg_anim_animate_and_move | âś… except reg_anim_callback | - |
| Art and appearance | art_exists<br>refresh_pc_art<br>art_cache_clear<br>set_hero_race<br>set_hero_style | implemented: art_exists, refresh_pc_art, art_cache_clear | - |
| Tiles and paths | get_tile_fid<br>tile_under_cursor<br>tile_light<br>tile_get_objs<br>tile_refresh_display<br>obj_blocking_tile<br>tile_by_position<br>get_tile_ground_fid<br>get_tile_roof_fid<br>obj_blocking_line<br>path_find_to<br>objects_in_radius | âś… except objects_in_radius | - |
| Tiles and paths | get_tile_fid<br>tile_under_cursor<br>tile_light<br>tile_get_objs<br>tile_refresh_display<br>obj_blocking_tile<br>tile_by_position<br>get_tile_ground_fid<br>get_tile_roof_fid<br>obj_blocking_line<br>path_find_to<br>objects_in_radius | âś… | - |
| Utility | sprintf<br>typeof<br>atoi<br>atof | âś… | - |
| Utility / Strings | string_split<br>substr<br>strlen<br>charcode<br>get_string_pointer<br>string_find<br>string_find_from<br>string_format<br>string_format_array<br>string_replace<br>string_to_case<br>string_compare | âś… | `get_string_pointer` is deprecated and intentionally omitted. |
| Interface / Tags | show_iface_tag<br>hide_iface_tag<br>is_iface_tag_active<br>set_iface_tag_text<br>add_iface_tag | âś… except set_iface_tag_text, add_iface_tag | CE only handles built-in interface tags here; custom tag creation/text is not supported yet. |
@@ -61,7 +61,7 @@ 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: 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 | - |
| 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 | - |
| 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 | âś… | - |
@@ -72,7 +72,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| 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>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, 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 | - |
| 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/<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 | - |
| 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. |
Binary file not shown.
+2
View File
@@ -96,6 +96,8 @@ display_bonus_damage=1
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
;Set to 1 to extend the action points bar to show up to 16 AP instead of 10 (requires iface_apbar_e.frm)
extend_ap_bar=0
iface_bar_mode=0
iface_bar_side_art=2
iface_bar_sides_ori=0
+28
View File
@@ -1,13 +1,21 @@
#include "sfall.h"
#include "define_extra.h"
#include "test_utils.h"
#define TEST_ITEM_PID 40
variable blocker_obj := 0;
variable radius_obj := 0;
procedure cleanup begin
if (blocker_obj) then begin
destroy_object(blocker_obj);
blocker_obj := 0;
end
if (radius_obj) then begin
destroy_object(radius_obj);
radius_obj := 0;
end
end
procedure start begin
@@ -45,6 +53,26 @@ procedure start begin
call assertEquals("one-step path length", len_array(path), 1);
if (len_array(path) == 1) then
call assertEquals("one-step path direction", get_array(path, 0), step_dir);
radius_obj := create_object_sid(TEST_ITEM_PID, step_tile, elev, -1);
call assertNotEquals("spawned radius item", radius_obj, 0);
if (radius_obj) then begin
objs := sfall_func3("objects_in_radius", dude_tile, 1, elev);
call assertTrue("objects_in_radius all contains dude", scan_array(objs, dude_obj) != -1);
call assertTrue("objects_in_radius all contains item", scan_array(objs, radius_obj) != -1);
objs := sfall_func3("objects_in_radius", dude_tile, 0, elev);
call assertTrue("objects_in_radius radius 0 contains dude", scan_array(objs, dude_obj) != -1);
call assertEquals("objects_in_radius radius 0 excludes adjacent item", scan_array(objs, radius_obj), -1);
objs := objects_in_radius(dude_tile, 1, elev, OBJ_TYPE_ITEM);
call assertTrue("objects_in_radius item filter contains item", scan_array(objs, radius_obj) != -1);
call assertEquals("objects_in_radius item filter excludes dude", scan_array(objs, dude_obj), -1);
objs := objects_in_radius(dude_tile, 1, elev, OBJ_TYPE_CRITTER);
call assertTrue("objects_in_radius critter filter contains dude", scan_array(objs, dude_obj) != -1);
call assertEquals("objects_in_radius critter filter excludes item", scan_array(objs, radius_obj), -1);
end
end
if (block_tile != -1) then begin
+11 -2
View File
@@ -11,7 +11,7 @@
#include "draw.h"
#include "game.h"
#include "memory.h"
#include "object.h"
#include "obj_types.h"
#include "proto.h"
#include "settings.h"
@@ -821,7 +821,7 @@ unsigned char* artGetFrameData(Art* art, int frame, int direction)
}
// 0x419880
ArtFrame* artGetFrame(Art* art, int frame, int rotation)
ArtFrame* artGetFrame(const Art* art, int frame, int rotation)
{
if (rotation < 0 || rotation >= 6) {
return nullptr;
@@ -842,6 +842,15 @@ ArtFrame* artGetFrame(Art* art, int frame, int rotation)
return frm;
}
ConstBuffer2D artGetFrameBuffer(const Art* art, int frame, int direction)
{
ArtFrame* frm = artGetFrame(art, frame, direction);
if (frm == nullptr) {
return { nullptr, 0, 0 };
}
return { reinterpret_cast<unsigned char*>(frm) + sizeof(*frm), frm->width, frm->height };
}
// 0x4198C8
bool artExists(int fid)
{
+8 -5
View File
@@ -2,10 +2,8 @@
#define ART_H
#include "cache.h"
#include "db.h"
#include "heap.h"
#include "obj_types.h"
#include "platform_compat.h"
#include "draw.h"
#include "memory.h"
#include "proto_types.h"
namespace fallout {
@@ -141,7 +139,8 @@ int artGetSize(Art* art, int frame, int direction, int* out_width, int* out_heig
int artGetFrameOffsets(Art* art, int frame, int direction, int* xPtr, int* yPtr);
int artGetRotationOffsets(Art* art, int rotation, int* out_offset_x, int* out_offset_y);
unsigned char* artGetFrameData(Art* art, int frame, int direction);
ArtFrame* artGetFrame(Art* art, int frame, int direction);
ArtFrame* artGetFrame(const Art* art, int frame, int direction);
ConstBuffer2D artGetFrameBuffer(const Art* art, int frame, int direction);
bool artExists(int fid);
bool _art_fid_valid(int fid);
int _art_alias_num(int index);
@@ -165,6 +164,8 @@ public:
int getHeight() const { return _height; }
unsigned char* getData() const { return _data; }
ConstBuffer2D getBuffer() const { return { _data, _width, _height }; };
private:
CacheEntry* _key;
unsigned char* _data;
@@ -172,6 +173,8 @@ private:
int _height;
};
using ArtPtr = InternalPtr<Art>;
} // namespace fallout
#endif
-20
View File
@@ -331,26 +331,6 @@ bool configRead(Config* config, const char* filePath, bool isDb)
configParseLine(config, string);
}
fileClose(stream);
// Build patch file path by inserting "#patch" before the extension.
char patchPath[COMPAT_MAX_PATH];
const char* dot = strrchr(filePath, '.');
if (dot != nullptr) {
snprintf(patchPath, sizeof(patchPath), "%.*s#patch%s", (int)(dot - filePath), filePath, dot);
} else {
snprintf(patchPath, sizeof(patchPath), "%s#patch", filePath);
}
struct PatchContext {
Config* config;
char string[CONFIG_FILE_MAX_LINE_LENGTH];
} patchCtx = { config };
xfileOpenEachReverse(patchPath, "rb", [](XFile* file, void* ctx) {
auto* pc = static_cast<PatchContext*>(ctx);
while (fileReadString(pc->string, sizeof(pc->string), file) != nullptr) {
configParseLine(pc->config, pc->string);
} }, &patchCtx);
} else {
FILE* stream = compat_fopen(filePath, "rt");
+3
View File
@@ -23,6 +23,9 @@ void contentConfigInit()
}
configRead(&gContentConfig, kConfigPath, true);
// Patch config allows to override only certain fields, without replacing the whole file.
// TODO: remove this after config patching by mods is implemented inside configRead
configRead(&gContentConfig, kConfigPatchPath, true);
}
void contentConfigExit()
+55 -1
View File
@@ -216,6 +216,60 @@ void blitBufferToBufferTrans(unsigned char* src, int width, int height, int srcP
transSrcCopy(dest, destPitch, src, srcPitch, width, height);
}
void blitBuffer2D(ConstBuffer2D src, Buffer2D dst, int dstX, int dstY)
{
blitBuffer2D(src, 0, 0, src.width, src.height, dst, dstX, dstY);
}
void blitBuffer2D(ConstBuffer2D src, int srcX, int srcY, int width, int height,
Buffer2D dst, int dstX, int dstY)
{
// Clip source region to src bounds.
if (srcX < 0) {
width += srcX;
dstX -= srcX;
srcX = 0;
}
if (srcY < 0) {
height += srcY;
dstY -= srcY;
srcY = 0;
}
if (srcX + width > src.width) {
width = src.width - srcX;
}
if (srcY + height > src.height) {
height = src.height - srcY;
}
// Clip destination region to dst bounds.
if (dstX < 0) {
srcX -= dstX;
width += dstX;
dstX = 0;
}
if (dstY < 0) {
srcY -= dstY;
height += dstY;
dstY = 0;
}
if (dstX + width > dst.width) {
width = dst.width - dstX;
}
if (dstY + height > dst.height) {
height = dst.height - dstY;
}
if (width <= 0 || height <= 0) {
return;
}
srcCopy(
dst.data + dstY * dst.width + dstX, dst.width,
src.data + srcY * src.width + srcX, src.width,
width, height);
}
// 0x4D387C
void bufferFill(unsigned char* buf, int width, int height, int pitch, int value)
{
@@ -321,7 +375,7 @@ void bufferOutline(unsigned char* buf, int width, int height, int pitch, int col
}
// 0x4E0DB0
void srcCopy(unsigned char* dest, int destPitch, unsigned char* src, int srcPitch, int width, int height)
void srcCopy(unsigned char* dest, int destPitch, const unsigned char* src, int srcPitch, int width, int height)
{
for (int y = 0; y < height; y++) {
memcpy(dest, src, width);
+40 -1
View File
@@ -1,8 +1,47 @@
#ifndef DRAW_H
#define DRAW_H
#include <type_traits>
namespace fallout {
template <typename T>
struct Buffer2DBase {
T* data = nullptr;
int width = 0;
int height = 0;
Buffer2DBase() = default;
Buffer2DBase(T* data, int width, int height)
: data(data)
, width(width)
, height(height)
{
}
// Implicit conversion Buffer2D -> ConstBuffer2D.
template <typename U, typename = std::enable_if_t<std::is_same_v<const U, T>>>
Buffer2DBase(const Buffer2DBase<U>& other)
: data(other.data)
, width(other.width)
, height(other.height)
{
}
explicit operator bool() const { return data != nullptr; }
};
using Buffer2D = Buffer2DBase<unsigned char>;
using ConstBuffer2D = Buffer2DBase<const unsigned char>;
// Copies src into dst at (dstX, dstY). Clips to dst bounds; no-op if fully outside.
void blitBuffer2D(ConstBuffer2D src, Buffer2D dst, int dstX = 0, int dstY = 0);
// Copies a region of src (starting at srcX, srcY with given width/height) into
// dst at (dstX, dstY). Clips to both src and dst bounds.
void blitBuffer2D(ConstBuffer2D src, int srcX, int srcY, int width, int height,
Buffer2D dst, int dstX = 0, int dstY = 0);
void bufferDrawLine(unsigned char* buf, int pitch, int left, int top, int right, int bottom, int color);
void bufferDrawRect(unsigned char* buf, int pitch, int left, int top, int right, int bottom, int color);
void bufferDrawRectShadowed(unsigned char* buf, int pitch, int left, int top, int right, int bottom, int ltColor, int rbColor);
@@ -16,7 +55,7 @@ void _buf_texture(unsigned char* buf, int width, int height, int pitch, void* te
void _lighten_buf(unsigned char* buf, int width, int height, int pitch);
void _swap_color_buf(unsigned char* buf, int width, int height, int pitch, int color1, int color2);
void bufferOutline(unsigned char* buf, int width, int height, int pitch, int color);
void srcCopy(unsigned char* dest, int destPitch, unsigned char* src, int srcPitch, int width, int height);
void srcCopy(unsigned char* dest, int destPitch, const unsigned char* src, int srcPitch, int width, int height);
void transSrcCopy(unsigned char* dest, int destPitch, unsigned char* src, int srcPitch, int width, int height);
} // namespace fallout
+21 -11
View File
@@ -26,8 +26,8 @@ namespace {
static bool gameConfigHasKey(Config* config, const char* section, const char* key);
static bool gameConfigNeedsF2ResMigration(Config* gameConfig);
static bool gameConfigMigrateStringKey(Config* legacyConfig, Config* gameConfig, const F2ResMigrationEntry& entry);
static bool gameConfigMigrateScaleKey(Config* legacyConfig, Config* gameConfig);
static bool gameConfigMigrateStringKey(Config* legacyConfig, Config* gameConfig, Config* targetConfig, const F2ResMigrationEntry& entry);
static bool gameConfigMigrateScaleKey(Config* legacyConfig, Config* gameConfig, Config* targetConfig);
static constexpr F2ResMigrationEntry kF2ResMigrationEntries[] = {
{ "MAIN", "SCR_WIDTH", GAME_CONFIG_SCREEN_KEY, GAME_CONFIG_RESOLUTION_X_KEY },
@@ -54,9 +54,9 @@ namespace {
return !gameConfigHasKey(gameConfig, GAME_CONFIG_SCREEN_KEY, GAME_CONFIG_RESOLUTION_X_KEY);
}
static bool gameConfigMigrateStringKey(Config* legacyConfig, Config* gameConfig, const F2ResMigrationEntry& entry)
static bool gameConfigMigrateStringKey(Config* legacyConfig, Config* gameConfig, Config* targetConfig, const F2ResMigrationEntry& entry)
{
assert(legacyConfig != nullptr && gameConfig != nullptr);
assert(legacyConfig != nullptr && gameConfig != nullptr && targetConfig != nullptr);
if (gameConfigHasKey(gameConfig, entry.targetSection, entry.targetKey)) {
return false;
@@ -67,12 +67,12 @@ namespace {
return false;
}
return configSetString(gameConfig, entry.targetSection, entry.targetKey, value);
return configSetString(targetConfig, entry.targetSection, entry.targetKey, value);
}
static bool gameConfigMigrateScaleKey(Config* legacyConfig, Config* gameConfig)
static bool gameConfigMigrateScaleKey(Config* legacyConfig, Config* gameConfig, Config* targetConfig)
{
assert(legacyConfig != nullptr && gameConfig != nullptr);
assert(legacyConfig != nullptr && gameConfig != nullptr && targetConfig != nullptr);
if (gameConfigHasKey(gameConfig, GAME_CONFIG_SCREEN_KEY, GAME_CONFIG_SCALE_KEY)) {
return false;
@@ -83,7 +83,7 @@ namespace {
return false;
}
return configSetInt(gameConfig, GAME_CONFIG_SCREEN_KEY, GAME_CONFIG_SCALE_KEY, value + 1);
return configSetInt(targetConfig, GAME_CONFIG_SCREEN_KEY, GAME_CONFIG_SCALE_KEY, value + 1);
}
} // namespace
@@ -97,7 +97,15 @@ bool gameConfigMigrateFromF2Res(const char* gameConfigFilePath, Config* gameConf
return false;
}
if (!gameConfigNeedsF2ResMigration(gameConfig)) {
Config fileConfig;
if (!configInit(&fileConfig)) {
return false;
}
configRead(&fileConfig, gameConfigFilePath, false);
if (!gameConfigNeedsF2ResMigration(&fileConfig)) {
configFree(&fileConfig);
return false;
}
@@ -109,23 +117,25 @@ bool gameConfigMigrateFromF2Res(const char* gameConfigFilePath, Config* gameConf
Config legacyConfig;
if (!configInit(&legacyConfig)) {
configFree(&fileConfig);
return false;
}
bool migrated = false;
if (configRead(&legacyConfig, f2ResFilePath, false)) {
for (const auto& entry : kF2ResMigrationEntries) {
if (gameConfigMigrateStringKey(&legacyConfig, gameConfig, entry)) {
if (gameConfigMigrateStringKey(&legacyConfig, &fileConfig, gameConfig, entry)) {
migrated = true;
}
}
if (gameConfigMigrateScaleKey(&legacyConfig, gameConfig)) {
if (gameConfigMigrateScaleKey(&legacyConfig, &fileConfig, gameConfig)) {
migrated = true;
}
}
configFree(&legacyConfig);
configFree(&fileConfig);
return migrated;
}
+97 -26
View File
@@ -128,6 +128,8 @@ static bool indicatorBarAdd(int indicator);
static void customInterfaceBarInit();
static void customInterfaceBarExit();
static void extendedApBarInitToWindow();
static void sidePanelsInit();
static void sidePanelsExit();
static void sidePanelsHide();
@@ -190,9 +192,6 @@ static int gEndTurnButton = -1;
// 0x518FBC
static int gEndCombatButton = -1;
// 0x518FD4
static Rect gInterfaceBarActionPointsBarRect;
// 0x518FE8
static IndicatorDescription gIndicatorDescriptions[INDICATOR_COUNT] = {
{ 102, true, nullptr }, // ADDICT
@@ -249,14 +248,34 @@ static unsigned char _itemButtonUp[INTERFACE_ITEM_ACTION_BUTTON_WIDTH * INTERFAC
// 0x59D3F4
static unsigned char* gInterfaceWindowBuffer;
// A slice of main interface background containing 10 shadowed action point
// Rectangle within Interface Bar window covering the Action Points bar.
// 0x518FD4
static Rect apBarRect;
// Width and height of AP bulbs.
constexpr int kApBarBulbSize = 5;
// Horizontal margin between AP bulbs.
constexpr int kApBarBulbMargin = 4;
constexpr int kApBarMaxBulbs = 16;
constexpr int kApBarMaxWidth = (kApBarBulbSize + kApBarBulbMargin) * kApBarMaxBulbs;
// A slice of main interface background containing up to 16 shadowed action point
// dots. In combat mode individual colored dots are rendered on top of this
// background.
//
// This buffer is initialized once and does not change throughout the game.
//
// 0x59D40C
static unsigned char gInterfaceActionPointsBarBackground[90 * 5];
static unsigned char apBarBackgroundData[kApBarMaxWidth * kApBarBulbSize];
static int apBarMaxAP;
static int apBarWidth;
// AP bar offset relative to the content area of the Interface Bar window.
static int apBarXOffset;
constexpr int kApBarYOffset = 14;
static FrmImage _inventoryButtonNormalFrmImage;
static FrmImage _inventoryButtonPressedFrmImage;
@@ -287,15 +306,26 @@ static FrmImage _greenLightFrmImage;
static FrmImage _yellowLightFrmImage;
static FrmImage _redLightFrmImage;
// X offset of interface bar content (used when widescreen version of iface bar is used).
// TODO: this seems like a bad solution, maybe better to separate message box and other content into two separate windows and avoid this offset?
int gInterfaceBarContentOffset = 0;
int gInterfaceBarWidth = 800; // will fall back to 640 if screen width is too narrow or asset is absent
bool gInterfaceBarIsCustom = false;
static Art* gCustomInterfaceBarBackground = nullptr;
bool gInterfaceSidePanelsExtendFromScreenEdge = false;
static int gInterfaceSidePanelsLeadingWindow = -1;
static int gInterfaceSidePanelsTrailingWindow = -1;
static Buffer2D interfaceWindowBuf2D()
{
return { gInterfaceWindowBuffer, gInterfaceBarWidth, INTERFACE_BAR_HEIGHT };
}
static Buffer2D apBarBackgroundBuf2D()
{
return { apBarBackgroundData, apBarWidth, kApBarBulbSize };
}
// intface_init
// 0x45D880
int interfaceInit()
@@ -308,7 +338,6 @@ int interfaceInit()
customInterfaceBarInit();
gInterfaceBarActionPointsBarRect = { 316 + gInterfaceBarContentOffset, 14, 406 + gInterfaceBarContentOffset, 19 };
gInterfaceBarEndButtonsRect = { 580 + gInterfaceBarContentOffset, 38, 637 + gInterfaceBarContentOffset, 96 };
gInterfaceBarMainActionRect = { 267 + gInterfaceBarContentOffset, 26, 455 + gInterfaceBarContentOffset, 93 };
@@ -329,6 +358,7 @@ int interfaceInit()
return intface_fatal_error(-1);
}
// Blit interface bar FRM into static window buffer.
if (gInterfaceBarIsCustom) {
blitBufferToBuffer(customInterfaceBarGetBackgroundImageData(), gInterfaceBarWidth, INTERFACE_BAR_HEIGHT - 1, gInterfaceBarWidth, gInterfaceWindowBuffer, gInterfaceBarWidth);
} else {
@@ -342,6 +372,8 @@ int interfaceInit()
backgroundFrmImage.unlock();
}
extendedApBarInitToWindow();
fid = buildFid(OBJ_TYPE_INTERFACE, 47, 0, 0, 0);
if (!_inventoryButtonNormalFrmImage.lock(fid)) {
// NOTE: Uninline.
@@ -560,8 +592,6 @@ int interfaceInit()
return intface_fatal_error(-1);
}
blitBufferToBuffer(gInterfaceWindowBuffer + gInterfaceBarWidth * 14 + 316 + gInterfaceBarContentOffset, 90, 5, gInterfaceBarWidth, gInterfaceActionPointsBarBackground, 90);
if (indicatorBarInit() == -1) {
// NOTE: Uninline.
return intface_fatal_error(-1);
@@ -970,49 +1000,49 @@ void interfaceRenderArmorClass(bool animate)
// 0x45EE0C
void interfaceRenderActionPoints(int actionPointsLeft, int bonusActionPoints)
{
unsigned char* frmData;
ConstBuffer2D bulbFrmBuf {};
if (gInterfaceBarWindow == -1) {
return;
}
blitBufferToBuffer(gInterfaceActionPointsBarBackground, 90, 5, 90, gInterfaceWindowBuffer + 14 * gInterfaceBarWidth + gInterfaceBarContentOffset + 316, gInterfaceBarWidth);
blitBuffer2D(apBarBackgroundBuf2D(), interfaceWindowBuf2D(), gInterfaceBarContentOffset + apBarXOffset, kApBarYOffset);
if (actionPointsLeft == -1) {
frmData = _redLightFrmImage.getData();
actionPointsLeft = 10;
bulbFrmBuf = _redLightFrmImage.getBuffer();
actionPointsLeft = apBarMaxAP;
bonusActionPoints = 0;
} else {
frmData = _greenLightFrmImage.getData();
bulbFrmBuf = _greenLightFrmImage.getBuffer();
if (actionPointsLeft < 0) {
actionPointsLeft = 0;
}
if (actionPointsLeft > 10) {
actionPointsLeft = 10;
if (actionPointsLeft > apBarMaxAP) {
actionPointsLeft = apBarMaxAP;
}
if (bonusActionPoints >= 0) {
if (actionPointsLeft + bonusActionPoints > 10) {
bonusActionPoints = 10 - actionPointsLeft;
if (actionPointsLeft + bonusActionPoints > apBarMaxAP) {
bonusActionPoints = apBarMaxAP - actionPointsLeft;
}
} else {
bonusActionPoints = 0;
}
}
int index;
for (index = 0; index < actionPointsLeft; index++) {
blitBufferToBuffer(frmData, 5, 5, 5, gInterfaceWindowBuffer + 14 * gInterfaceBarWidth + 316 + index * 9 + gInterfaceBarContentOffset, gInterfaceBarWidth);
}
for (; index < (actionPointsLeft + bonusActionPoints); index++) {
blitBufferToBuffer(_yellowLightFrmImage.getData(), 5, 5, 5, gInterfaceWindowBuffer + 14 * gInterfaceBarWidth + 316 + gInterfaceBarContentOffset + index * 9, gInterfaceBarWidth);
int numBulbs = actionPointsLeft + bonusActionPoints;
for (int index = 0; index < numBulbs; index++) {
constexpr int bulbXOffset = kApBarBulbSize + kApBarBulbMargin;
auto frmBuf = index < actionPointsLeft
? bulbFrmBuf
: _yellowLightFrmImage.getBuffer();
blitBuffer2D(frmBuf, 0, 0, kApBarBulbSize, kApBarBulbSize, interfaceWindowBuf2D(), gInterfaceBarContentOffset + apBarXOffset + index * bulbXOffset, kApBarYOffset);
}
if (!gInterfaceBarInitialized) {
windowRefreshRect(gInterfaceBarWindow, &gInterfaceBarActionPointsBarRect);
windowRefreshRect(gInterfaceBarWindow, &apBarRect);
}
}
@@ -2496,6 +2526,47 @@ static void customInterfaceBarExit()
}
}
// Inits AP bar offsets based on custom AP bar setting and blits the correct AP graphic into the window buffer. Must be called after main panel FRM art is blitted.
static void extendedApBarInitToWindow()
{
constexpr int apBarXOffsetOriginal = 316;
constexpr int apBarWidthOriginal = 90;
constexpr int apBarMaxAPOriginal = 10;
static_assert(apBarWidthOriginal <= kApBarMaxWidth);
static_assert(apBarMaxAPOriginal <= kApBarMaxBulbs);
if (settings.ui.extend_ap_bar) {
apBarMaxAP = kApBarMaxBulbs;
apBarWidth = kApBarMaxWidth;
// Shift X offset left according to the extra AP bar width.
apBarXOffset = apBarXOffsetOriginal - (kApBarMaxWidth - apBarWidthOriginal) / 2;
} else {
apBarMaxAP = apBarMaxAPOriginal;
apBarWidth = apBarWidthOriginal;
apBarXOffset = apBarXOffsetOriginal;
}
apBarRect = { gInterfaceBarContentOffset + apBarXOffset, kApBarYOffset, gInterfaceBarContentOffset + apBarXOffset + apBarWidth - 1, kApBarYOffset + kApBarBulbSize - 1 };
Buffer2D ifaceBarBuf = interfaceWindowBuf2D();
// Blit extended AP bar art into static window buffer, overwriting pixels from the interface bar FRM.
if (settings.ui.extend_ap_bar) {
if (ArtPtr apBarArt { artLoad("art\\intrface\\iface_apbar_e.frm") }) {
if (auto apBarFrmBuf = artGetFrameBuffer(apBarArt.get(), 0, 0)) {
int apBarBgXOffset = apBarXOffset - 23;
constexpr int apBarBgYOffset = kApBarYOffset - 4;
blitBuffer2D(apBarFrmBuf, ifaceBarBuf, gInterfaceBarContentOffset + apBarBgXOffset, apBarBgYOffset);
}
}
}
// Blit the thin bar of pixels covering the bulbs (that change color) into static apBarBackgroundData buffer.
Buffer2D abBarBgBuf = apBarBackgroundBuf2D();
blitBuffer2D(ifaceBarBuf, gInterfaceBarContentOffset + apBarXOffset, kApBarYOffset, abBarBgBuf.width, abBarBgBuf.height, abBarBgBuf);
}
unsigned char* customInterfaceBarGetBackgroundImageData()
{
if (!gInterfaceBarIsCustom) {
-1
View File
@@ -34,7 +34,6 @@ extern bool gInterfaceBarMode;
extern int gInterfaceBarWidth;
extern bool gInterfaceBarIsCustom;
extern int gInterfaceBarContentOffset;
extern bool gInterfaceSidePanelsExtendFromScreenEdge;
int interfaceInit();
void interfaceReset();
+1
View File
@@ -5,6 +5,7 @@
#include <string.h>
#include "debug.h"
#include "memory_defs.h"
namespace fallout {
+51 -1
View File
@@ -1,7 +1,8 @@
#ifndef MEMORY_H
#define MEMORY_H
#include "memory_defs.h"
#include <cstddef>
#include <type_traits>
namespace fallout {
@@ -11,6 +12,55 @@ void* internal_realloc(void* ptr, size_t size);
void internal_free(void* ptr);
void mem_check();
// Owning smart pointer for objects allocated with internal_malloc.
template <typename T>
class InternalPtr {
public:
static_assert(std::is_trivially_destructible_v<T>);
InternalPtr() = default;
explicit InternalPtr(T* ptr)
: _ptr(ptr)
{
}
~InternalPtr() { reset(); }
InternalPtr(const InternalPtr&) = delete;
InternalPtr& operator=(const InternalPtr&) = delete;
InternalPtr(InternalPtr&& other) noexcept
: _ptr(other._ptr)
{
other._ptr = nullptr;
}
InternalPtr& operator=(InternalPtr&& other) noexcept
{
if (this != &other) {
reset();
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
void reset()
{
if (_ptr != nullptr) {
internal_free(_ptr);
_ptr = nullptr;
}
}
T* get() const { return _ptr; }
T* operator->() const { return _ptr; }
T& operator*() const { return *_ptr; }
explicit operator bool() const { return _ptr != nullptr; }
private:
T* _ptr = nullptr;
};
} // namespace fallout
#endif /* MEMORY_H */
+1 -1
View File
@@ -556,7 +556,7 @@ static void* ioRead(int size)
return nullptr;
}
if (mve_read_func(io_handle, buf, size) < 1) {
if (!mve_read_func(io_handle, buf, size)) {
return nullptr;
}

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