Compare commits

..
Author SHA1 Message Date
Mike Klaas 80bdd7fdb0 remove isIniatlized() 2026-05-29 18:51:53 -07:00
Mike Klaas 4eb9e869df outdent one more place 2026-05-27 08:34:15 -07:00
Mike Klaas 49a24f6925 Use ScopeConfig for function-scoped Config uses
Eliminates the free, which sometimes required gotos.

Left configRead in a couple places that return different values if the file is missing (0) vs hard error (-1). With more careful analysis it's possible we could fold the read into the ScopeConfig constructor in these cases too.

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

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

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

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