mirror of
https://github.com/fallout2-ce/fallout2-ce.git
synced 2026-07-27 16:47:11 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dab1e6219 | ||
|
|
dcba8882bf | ||
|
|
62c2621fef | ||
|
|
5188ebd4f4 |
@@ -10,6 +10,7 @@
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.DS_Store
|
||||
.local-tools/
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -119,9 +119,6 @@
|
||||
"base"
|
||||
],
|
||||
"generator": "Xcode",
|
||||
"cacheVariables": {
|
||||
"CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY": ""
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -82,7 +82,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
|
||||
| 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. |
|
||||
|
||||
@@ -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
|
||||
@@ -4938,6 +4938,7 @@ static void _damage_object(Object* a1, int damage, bool animated, int a4, Object
|
||||
}
|
||||
|
||||
partyMemberRemove(a1);
|
||||
scriptHooks_OnDeath(a1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -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;
|
||||
|
||||
@@ -60,10 +60,6 @@ bool mouseDeviceInitMode()
|
||||
return SDL_SetRelativeMouseMode(SDL_TRUE) == 0;
|
||||
}
|
||||
|
||||
if (SDL_SetRelativeMouseMode(SDL_FALSE) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mouseRelativeMode = false;
|
||||
mouseDeviceRefreshWindowMapping();
|
||||
return true;
|
||||
|
||||
+115
-133
@@ -110,155 +110,137 @@ void registerSetting(const char* section, const char* key, T& variable, P postPr
|
||||
[&, section, key]() { settingsWrite(section, key, variable); } });
|
||||
}
|
||||
|
||||
struct SettingEntry {
|
||||
std::function<void(const char*)> registerFunc;
|
||||
|
||||
template <typename T>
|
||||
SettingEntry(const char* key, T& variable)
|
||||
: registerFunc([key, &variable](const char* section) { registerSetting(section, key, variable); })
|
||||
{
|
||||
}
|
||||
|
||||
template <typename T, typename P>
|
||||
SettingEntry(const char* key, T& variable, P postProcess)
|
||||
: registerFunc([key, &variable, postProcess](const char* section) { registerSetting(section, key, variable, postProcess); })
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
static void addSection(const char* section, const std::initializer_list<SettingEntry> entries)
|
||||
{
|
||||
for (const auto& entry : entries) {
|
||||
entry.registerFunc(section);
|
||||
}
|
||||
}
|
||||
// SECT must be defined to the settings sub-struct name, which equals the config section string.
|
||||
#define XSTR(x) #x
|
||||
#define STR(x) XSTR(x)
|
||||
#define SETTING(f) registerSetting(STR(SECT), #f, settings.SECT.f)
|
||||
#define SETTING_P(f, proc) registerSetting(STR(SECT), #f, settings.SECT.f, proc)
|
||||
#define SETTING_PATH(f) registerSetting(STR(SECT), #f, settings.SECT.f##_path, normalizePath)
|
||||
|
||||
void initSettingsRegistry(bool isMapper)
|
||||
{
|
||||
if (!settingsRegistry.empty()) return;
|
||||
|
||||
addSection(GAME_CONFIG_SYSTEM_KEY,
|
||||
{
|
||||
{ "executable", settings.system.executable },
|
||||
{ GAME_CONFIG_MASTER_DAT_KEY, settings.system.master_dat_path, normalizePath },
|
||||
{ GAME_CONFIG_MASTER_PATCHES_KEY, settings.system.master_patches_path, normalizePath },
|
||||
{ GAME_CONFIG_CRITTER_DAT_KEY, settings.system.critter_dat_path, normalizePath },
|
||||
{ GAME_CONFIG_CRITTER_PATCHES_KEY, settings.system.critter_patches_path, normalizePath },
|
||||
{ "language", settings.system.language },
|
||||
{ "scroll_lock", settings.system.scroll_lock },
|
||||
{ "interrupt_walk", settings.system.interrupt_walk },
|
||||
{ "art_cache_size", settings.system.art_cache_size },
|
||||
{ "color_cycling", settings.system.color_cycling },
|
||||
{ "cycle_speed_factor", settings.system.cycle_speed_factor },
|
||||
{ "hashing", settings.system.hashing },
|
||||
{ "splash", settings.system.splash },
|
||||
{ "free_space", settings.system.free_space },
|
||||
{ "screenshots_format", settings.system.screenshots_format },
|
||||
});
|
||||
#define SECT system
|
||||
SETTING(executable);
|
||||
SETTING_PATH(master_dat);
|
||||
SETTING_PATH(master_patches);
|
||||
SETTING_PATH(critter_dat);
|
||||
SETTING_PATH(critter_patches);
|
||||
SETTING(language);
|
||||
SETTING(scroll_lock);
|
||||
SETTING(interrupt_walk);
|
||||
SETTING(art_cache_size);
|
||||
SETTING(color_cycling);
|
||||
SETTING(cycle_speed_factor);
|
||||
SETTING(hashing);
|
||||
SETTING(splash);
|
||||
SETTING(free_space);
|
||||
SETTING(screenshots_format);
|
||||
#undef SECT
|
||||
|
||||
addSection(GAME_CONFIG_SCREEN_KEY,
|
||||
{
|
||||
{ GAME_CONFIG_RESOLUTION_X_KEY, settings.screen.resolution_x, clamp(640, 7680) },
|
||||
{ GAME_CONFIG_RESOLUTION_Y_KEY, settings.screen.resolution_y, clamp(480, 4320) },
|
||||
{ GAME_CONFIG_WINDOWED_KEY, settings.screen.windowed },
|
||||
{ GAME_CONFIG_SCALE_KEY, settings.screen.scale, clamp(1, 4) },
|
||||
});
|
||||
#define SECT screen
|
||||
SETTING_P(resolution_x, clamp(640, 7680));
|
||||
SETTING_P(resolution_y, clamp(480, 4320));
|
||||
SETTING(windowed);
|
||||
SETTING_P(scale, clamp(1, 4));
|
||||
#undef SECT
|
||||
|
||||
addSection(GAME_CONFIG_UI_KEY,
|
||||
{
|
||||
{ GAME_CONFIG_IFACE_BAR_MODE_KEY, settings.ui.iface_bar_mode },
|
||||
{ GAME_CONFIG_IFACE_BAR_WIDTH_KEY, settings.ui.iface_bar_width, clamp(640, 4320) },
|
||||
{ GAME_CONFIG_IFACE_BAR_SIDE_ART_KEY, settings.ui.iface_bar_side_art, clamp(0, 999) },
|
||||
{ GAME_CONFIG_IFACE_BAR_SIDES_ORI_KEY, settings.ui.iface_bar_sides_ori },
|
||||
{ GAME_CONFIG_SPLASH_SCREEN_SIZE_KEY, settings.ui.splash_screen_size, clamp(0, 2) },
|
||||
{ GAME_CONFIG_IGNORE_MAP_EDGES_KEY, settings.ui.ignore_map_edges },
|
||||
{ "anim_speed", settings.ui.anim_speed, clamp(0.1, 100.0) },
|
||||
{ "skip_opening_movies", settings.ui.skip_opening_movies, clamp(0, 2) },
|
||||
{ "display_karma_changes", settings.ui.display_karma_changes },
|
||||
{ "display_bonus_damage", settings.ui.display_bonus_damage },
|
||||
{ "numbers_in_dialogue", settings.ui.numbers_in_dialogue },
|
||||
{ "auto_quick_save", settings.ui.auto_quick_save, clamp(0, 10) },
|
||||
{ "enable_high_resolution_stencil", settings.ui.enable_high_resolution_stencil },
|
||||
});
|
||||
#define SECT ui
|
||||
SETTING(iface_bar_mode);
|
||||
SETTING_P(iface_bar_width, clamp(640, 4320));
|
||||
SETTING_P(iface_bar_side_art, clamp(0, 999));
|
||||
SETTING(iface_bar_sides_ori);
|
||||
SETTING_P(splash_screen_size, clamp(0, 2));
|
||||
SETTING(ignore_map_edges);
|
||||
SETTING_P(anim_speed, clamp(0.1, 100.0));
|
||||
SETTING_P(skip_opening_movies, clamp(0, 2));
|
||||
SETTING(display_karma_changes);
|
||||
SETTING(display_bonus_damage);
|
||||
SETTING(numbers_in_dialogue);
|
||||
SETTING_P(auto_quick_save, clamp(0, 10));
|
||||
SETTING(enable_high_resolution_stencil);
|
||||
#undef SECT
|
||||
|
||||
addSection("preferences",
|
||||
{
|
||||
// Clamping for most of these values is handled in preferences.cc
|
||||
{ "game_difficulty", settings.preferences.game_difficulty },
|
||||
{ "combat_difficulty", settings.preferences.combat_difficulty },
|
||||
{ "violence_level", settings.preferences.violence_level },
|
||||
{ "target_highlight", settings.preferences.target_highlight },
|
||||
{ "item_highlight", settings.preferences.item_highlight },
|
||||
{ "combat_looks", settings.preferences.combat_looks },
|
||||
{ "combat_messages", settings.preferences.combat_messages },
|
||||
{ "combat_taunts", settings.preferences.combat_taunts },
|
||||
{ "language_filter", settings.preferences.language_filter },
|
||||
{ "running", settings.preferences.running },
|
||||
{ "subtitles", settings.preferences.subtitles },
|
||||
{ "combat_speed", settings.preferences.combat_speed },
|
||||
{ "player_speedup", settings.preferences.player_speedup },
|
||||
{ "text_base_delay", settings.preferences.text_base_delay },
|
||||
{ "text_line_delay", settings.preferences.text_line_delay },
|
||||
{ "brightness", settings.preferences.brightness },
|
||||
{ "mouse_sensitivity", settings.preferences.mouse_sensitivity },
|
||||
{ "running_burning_guy", settings.preferences.running_burning_guy },
|
||||
});
|
||||
#define SECT preferences
|
||||
// Clamping for most of these values is handled in preferences.cc
|
||||
SETTING(game_difficulty);
|
||||
SETTING(combat_difficulty);
|
||||
SETTING(violence_level);
|
||||
SETTING(target_highlight);
|
||||
SETTING(item_highlight);
|
||||
SETTING(combat_looks);
|
||||
SETTING(combat_messages);
|
||||
SETTING(combat_taunts);
|
||||
SETTING(language_filter);
|
||||
SETTING(running);
|
||||
SETTING(subtitles);
|
||||
SETTING(combat_speed);
|
||||
SETTING(player_speedup);
|
||||
SETTING(text_base_delay);
|
||||
SETTING(text_line_delay);
|
||||
SETTING(brightness);
|
||||
SETTING(mouse_sensitivity);
|
||||
SETTING(running_burning_guy);
|
||||
#undef SECT
|
||||
|
||||
addSection(GAME_CONFIG_SOUND_KEY,
|
||||
{
|
||||
{ "initialize", settings.sound.initialize },
|
||||
{ "debug", settings.sound.debug },
|
||||
{ "debug_sfxc", settings.sound.debug_sfxc },
|
||||
{ "sounds", settings.sound.sounds },
|
||||
{ "music", settings.sound.music },
|
||||
{ "speech", settings.sound.speech },
|
||||
{ "master_volume", settings.sound.master_volume },
|
||||
{ "music_volume", settings.sound.music_volume },
|
||||
{ "sndfx_volume", settings.sound.sndfx_volume },
|
||||
{ "speech_volume", settings.sound.speech_volume },
|
||||
{ "cache_size", settings.sound.cache_size },
|
||||
{ GAME_CONFIG_MUSIC_PATH1_KEY, settings.sound.music_path1, normalizePath },
|
||||
{ GAME_CONFIG_MUSIC_PATH2_KEY, settings.sound.music_path2, normalizePath },
|
||||
{ "gapless_music", settings.sound.gapless_music },
|
||||
});
|
||||
#define SECT sound
|
||||
SETTING(initialize);
|
||||
SETTING(debug);
|
||||
SETTING(debug_sfxc);
|
||||
SETTING(sounds);
|
||||
SETTING(music);
|
||||
SETTING(speech);
|
||||
SETTING(master_volume);
|
||||
SETTING(music_volume);
|
||||
SETTING(sndfx_volume);
|
||||
SETTING(speech_volume);
|
||||
SETTING(cache_size);
|
||||
SETTING_P(music_path1, normalizePath);
|
||||
SETTING_P(music_path2, normalizePath);
|
||||
SETTING(gapless_music);
|
||||
#undef SECT
|
||||
|
||||
addSection(GAME_CONFIG_DEBUG_KEY,
|
||||
{
|
||||
{ GAME_CONFIG_MODE_KEY, settings.debug.mode },
|
||||
{ "show_tile_num", settings.debug.show_tile_num },
|
||||
{ "show_script_messages", settings.debug.show_script_messages },
|
||||
{ "show_load_info", settings.debug.show_load_info },
|
||||
{ "output_map_data_info", settings.debug.output_map_data_info },
|
||||
{ "window_width", settings.debug.debug_window_width, clamp(200, 1920) },
|
||||
{ "window_height", settings.debug.debug_window_height, clamp(100, 1080) },
|
||||
{ "console_output_path", settings.debug.console_output_path },
|
||||
});
|
||||
#define SECT debug
|
||||
SETTING(mode);
|
||||
SETTING(show_tile_num);
|
||||
SETTING(show_script_messages);
|
||||
SETTING(show_load_info);
|
||||
SETTING(output_map_data_info);
|
||||
SETTING_P(window_width, clamp(200, 1920));
|
||||
SETTING_P(window_height, clamp(100, 1080));
|
||||
SETTING(console_output_path);
|
||||
#undef SECT
|
||||
|
||||
addSection("qol",
|
||||
{
|
||||
{ "use_walk_distance", settings.qol.use_walk_distance, clamp(0, 100) },
|
||||
{ "auto_open_doors", settings.qol.auto_open_doors },
|
||||
});
|
||||
#define SECT qol
|
||||
SETTING_P(use_walk_distance, clamp(0, 100));
|
||||
SETTING(auto_open_doors);
|
||||
#undef SECT
|
||||
|
||||
if (isMapper) {
|
||||
addSection("mapper",
|
||||
{
|
||||
{ "override_librarian", settings.mapper.override_librarian },
|
||||
{ "librarian", settings.mapper.librarian },
|
||||
{ "use_art_not_protos", settings.mapper.user_art_not_protos },
|
||||
{ "rebuild_protos", settings.mapper.rebuild_protos },
|
||||
{ "fix_map_objects", settings.mapper.fix_map_objects },
|
||||
{ "fix_map_inventory", settings.mapper.fix_map_inventory },
|
||||
{ "ignore_rebuild_errors", settings.mapper.ignore_rebuild_errors },
|
||||
{ "show_pid_numbers", settings.mapper.show_pid_numbers },
|
||||
{ "save_text_maps", settings.mapper.save_text_maps },
|
||||
{ "run_mapper_as_game", settings.mapper.run_mapper_as_game },
|
||||
{ "default_f8_as_game", settings.mapper.default_f8_as_game },
|
||||
{ "sort_script_list", settings.mapper.sort_script_list },
|
||||
});
|
||||
#define SECT mapper
|
||||
SETTING(override_librarian);
|
||||
SETTING(librarian);
|
||||
SETTING(use_art_not_protos);
|
||||
SETTING(rebuild_protos);
|
||||
SETTING(fix_map_objects);
|
||||
SETTING(fix_map_inventory);
|
||||
SETTING(ignore_rebuild_errors);
|
||||
SETTING(show_pid_numbers);
|
||||
SETTING(save_text_maps);
|
||||
SETTING(run_mapper_as_game);
|
||||
SETTING(default_f8_as_game);
|
||||
SETTING(sort_script_list);
|
||||
#undef SECT
|
||||
}
|
||||
}
|
||||
|
||||
#undef SETTING
|
||||
#undef SETTING_P
|
||||
#undef SETTING_PATH
|
||||
#undef STR
|
||||
#undef XSTR
|
||||
|
||||
bool settingsInit(bool isMapper, int argc, char** argv)
|
||||
{
|
||||
initSettingsRegistry(isMapper);
|
||||
|
||||
+3
-3
@@ -109,8 +109,8 @@ struct DebugSettings {
|
||||
bool show_script_messages = false;
|
||||
bool show_load_info = false;
|
||||
bool output_map_data_info = false;
|
||||
int debug_window_width = 300;
|
||||
int debug_window_height = 192;
|
||||
int window_width = 300;
|
||||
int window_height = 192;
|
||||
std::string console_output_path = "";
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ struct QolSettings {
|
||||
struct MapperSettings {
|
||||
bool override_librarian = false;
|
||||
bool librarian = false;
|
||||
bool user_art_not_protos = false;
|
||||
bool use_art_not_protos = false;
|
||||
bool rebuild_protos = false;
|
||||
bool fix_map_objects = false;
|
||||
bool fix_map_inventory = false;
|
||||
|
||||
@@ -164,6 +164,16 @@ void scriptHooks_GameModeChange(int exit, int previousGameMode)
|
||||
ScriptHookCall(HOOK_GAMEMODECHANGE, 0, { exit, previousGameMode }).call();
|
||||
}
|
||||
|
||||
/*
|
||||
Runs immediately after a critter dies for any reason.
|
||||
|
||||
Critter arg0 - The critter that just died
|
||||
*/
|
||||
void scriptHooks_OnDeath(Object* critter)
|
||||
{
|
||||
ScriptHookCall(HOOK_ONDEATH, 0, { critter }).call();
|
||||
}
|
||||
|
||||
/*
|
||||
Runs before and after each turn in combat (for both PC and NPC).
|
||||
|
||||
|
||||
@@ -257,6 +257,7 @@ void scriptHooksReset();
|
||||
void scriptHooksExit();
|
||||
|
||||
void scriptHooks_GameModeChange(int exit, int previousGameMode);
|
||||
void scriptHooks_OnDeath(Object* critter);
|
||||
bool scriptHooks_InventoryMove(HookInventoryMoveType actionType, Object* item, Object* targetItem);
|
||||
bool scriptHooks_CombatTurnStart(Object* critter, bool reloadedDuringCombat);
|
||||
bool scriptHooks_CombatTurnEnd(Object* critter, int turnResult, bool reloadedDuringCombat);
|
||||
|
||||
@@ -821,8 +821,8 @@ int _win_debug(char* string)
|
||||
|
||||
int lineHeight = fontGetLineHeight();
|
||||
|
||||
int winWidth = settings.debug.debug_window_width;
|
||||
int winHeight = settings.debug.debug_window_height;
|
||||
int winWidth = settings.debug.window_width;
|
||||
int winHeight = settings.debug.window_height;
|
||||
if (_wd == -1) {
|
||||
_wd = windowCreate(80, 80, winWidth, winHeight, 256, WINDOW_MOVE_ON_TOP);
|
||||
if (_wd == -1) {
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
#include "dfile.h"
|
||||
#include "platform_compat.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
namespace fallout {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Options {
|
||||
std::string archivePath;
|
||||
std::string command;
|
||||
std::vector<std::string> args;
|
||||
bool lowerExtractedPaths = false;
|
||||
};
|
||||
|
||||
std::string normalizeDatPath(std::string path)
|
||||
{
|
||||
for (char& ch : path) {
|
||||
if (ch == '/') {
|
||||
ch = '\\';
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string datPathToNativePath(std::string_view path)
|
||||
{
|
||||
std::string value(path);
|
||||
for (char& ch : value) {
|
||||
if (ch == '\\') {
|
||||
ch = '/';
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string toLowerAscii(std::string value)
|
||||
{
|
||||
for (char& ch : value) {
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
bool isAbsoluteOutputPath(const std::string& path)
|
||||
{
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path[0] == '/' || path[0] == '\\') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return path.size() >= 2 && path[1] == ':';
|
||||
}
|
||||
|
||||
bool isSafeRelativeOutputPath(const std::string& path)
|
||||
{
|
||||
if (path.empty() || isAbsoluteOutputPath(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t start = 0;
|
||||
while (start < path.size()) {
|
||||
size_t end = path.find('/', start);
|
||||
if (end == std::string::npos) {
|
||||
end = path.size();
|
||||
}
|
||||
|
||||
std::string_view component(path.data() + start, end - start);
|
||||
if (component == "..") {
|
||||
return false;
|
||||
}
|
||||
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string joinNativePath(const std::string& basePath, const std::string& relativePath)
|
||||
{
|
||||
if (basePath.empty()) {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
if (basePath.back() == '/' || basePath.back() == '\\') {
|
||||
return basePath + relativePath;
|
||||
}
|
||||
|
||||
return basePath + "/" + relativePath;
|
||||
}
|
||||
|
||||
bool ensureDirectoriesForFile(const std::string& filePath)
|
||||
{
|
||||
size_t separator = filePath.find_last_of("/\\");
|
||||
if (separator == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string directoryPath = filePath.substr(0, separator);
|
||||
if (directoryPath.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t start = 0;
|
||||
if (directoryPath.size() >= 2 && directoryPath[1] == ':') {
|
||||
start = 2;
|
||||
if (directoryPath.size() >= 3 && (directoryPath[2] == '/' || directoryPath[2] == '\\')) {
|
||||
start = 3;
|
||||
}
|
||||
} else if (directoryPath[0] == '/' || directoryPath[0] == '\\') {
|
||||
start = 1;
|
||||
}
|
||||
|
||||
while (start <= directoryPath.size()) {
|
||||
size_t end = directoryPath.find_first_of("/\\", start);
|
||||
std::string partialPath = directoryPath.substr(0, end);
|
||||
if (!partialPath.empty() && compat_mkdir(partialPath.c_str()) != 0 && errno != EEXIST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void printUsage(std::ostream& stream)
|
||||
{
|
||||
stream
|
||||
<< "Usage:\n"
|
||||
<< " fallout2-dat <archive.dat> list [pattern]\n"
|
||||
<< " fallout2-dat <archive.dat> info [pattern]\n"
|
||||
<< " fallout2-dat <archive.dat> extract [--lower] <output-dir> [pattern]\n"
|
||||
<< " fallout2-dat <archive.dat> cat <entry>\n"
|
||||
<< "\n"
|
||||
<< "Notes:\n"
|
||||
<< " - This tool is currently read-only.\n"
|
||||
<< " - Patterns use the same Windows-style wildcard matching as the game.\n"
|
||||
<< " - Archive paths are case-insensitive and should use backslashes internally.\n";
|
||||
}
|
||||
|
||||
bool parseOptions(int argc, char** argv, Options* options)
|
||||
{
|
||||
if (argc < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
options->archivePath = argv[1];
|
||||
options->command = argv[2];
|
||||
options->args.assign(argv + 3, argv + argc);
|
||||
|
||||
if (options->command == "extract") {
|
||||
auto lowerIt = std::find(options->args.begin(), options->args.end(), "--lower");
|
||||
if (lowerIt != options->args.end()) {
|
||||
options->lowerExtractedPaths = true;
|
||||
options->args.erase(lowerIt);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool extractEntry(DBase* dbase, const DBaseEntry& entry, const std::string& outputDir, bool lowerExtractedPaths)
|
||||
{
|
||||
std::string relativePath = datPathToNativePath(entry.path);
|
||||
if (lowerExtractedPaths) {
|
||||
relativePath = toLowerAscii(std::move(relativePath));
|
||||
}
|
||||
|
||||
if (!isSafeRelativeOutputPath(relativePath)) {
|
||||
std::cerr << "Refusing to extract invalid path: " << entry.path << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string destination = joinNativePath(outputDir, relativePath);
|
||||
if (!ensureDirectoriesForFile(destination)) {
|
||||
std::cerr << "Failed to create output directory for: " << destination << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
DFile* stream = dfileOpen(dbase, entry.path, "rb");
|
||||
if (stream == nullptr) {
|
||||
std::cerr << "Failed to open entry: " << entry.path << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream output(destination, std::ios::binary);
|
||||
if (!output.is_open()) {
|
||||
std::cerr << "Failed to create output file: " << destination << "\n";
|
||||
dfileClose(stream);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> buffer(64 * 1024);
|
||||
long remaining = dfileGetSize(stream);
|
||||
while (remaining > 0) {
|
||||
size_t chunkSize = static_cast<size_t>(std::min<long>(remaining, buffer.size()));
|
||||
size_t bytesRead = dfileRead(buffer.data(), 1, chunkSize, stream);
|
||||
if (bytesRead == 0) {
|
||||
std::cerr << "Failed while reading entry: " << entry.path << "\n";
|
||||
dfileClose(stream);
|
||||
return false;
|
||||
}
|
||||
|
||||
output.write(buffer.data(), static_cast<std::streamsize>(bytesRead));
|
||||
if (!output) {
|
||||
std::cerr << "Failed while writing output file: " << destination << "\n";
|
||||
dfileClose(stream);
|
||||
return false;
|
||||
}
|
||||
|
||||
remaining -= static_cast<long>(bytesRead);
|
||||
}
|
||||
|
||||
if (dfileClose(stream) != 0) {
|
||||
std::cerr << "Failed to close entry stream cleanly: " << entry.path << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << entry.path << " -> " << destination << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
int listCommand(DBase* dbase, const std::string& pattern)
|
||||
{
|
||||
DFileFindData findData;
|
||||
if (!dbaseFindFirstEntry(dbase, &findData, pattern.c_str())) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
do {
|
||||
std::cout << findData.fileName << "\n";
|
||||
} while (dbaseFindNextEntry(dbase, &findData));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int infoCommand(DBase* dbase, const std::vector<std::string>& args)
|
||||
{
|
||||
if (args.empty()) {
|
||||
long long compressedBytes = 0;
|
||||
long long uncompressedBytes = 0;
|
||||
int compressedEntries = 0;
|
||||
|
||||
for (int index = 0; index < dbase->entriesLength; index++) {
|
||||
const DBaseEntry& entry = dbase->entries[index];
|
||||
compressedBytes += entry.dataSize;
|
||||
uncompressedBytes += entry.uncompressedSize;
|
||||
if (entry.compressed == 1) {
|
||||
compressedEntries++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout
|
||||
<< "archive: " << dbase->path << "\n"
|
||||
<< "entries: " << dbase->entriesLength << "\n"
|
||||
<< "data_offset: " << dbase->dataOffset << "\n"
|
||||
<< "compressed_entries: " << compressedEntries << "\n"
|
||||
<< "stored_bytes: " << compressedBytes << "\n"
|
||||
<< "uncompressed_bytes: " << uncompressedBytes << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string pattern = normalizeDatPath(args[0]);
|
||||
DFileFindData findData;
|
||||
if (!dbaseFindFirstEntry(dbase, &findData, pattern.c_str())) {
|
||||
std::cerr << "No entries matched pattern: " << pattern << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
do {
|
||||
const DBaseEntry* entry = &dbase->entries[findData.index];
|
||||
std::cout
|
||||
<< entry->path
|
||||
<< "\tcompressed=" << static_cast<int>(entry->compressed)
|
||||
<< "\tstored=" << entry->dataSize
|
||||
<< "\tuncompressed=" << entry->uncompressedSize
|
||||
<< "\toffset=" << entry->dataOffset
|
||||
<< "\n";
|
||||
} while (dbaseFindNextEntry(dbase, &findData));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int extractCommand(DBase* dbase, const std::vector<std::string>& args, bool lowerExtractedPaths)
|
||||
{
|
||||
if (args.empty()) {
|
||||
std::cerr << "extract requires an output directory\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string outputDir = args[0];
|
||||
std::string pattern = "*";
|
||||
if (args.size() >= 2) {
|
||||
pattern = normalizeDatPath(args[1]);
|
||||
}
|
||||
|
||||
if (compat_mkdir(outputDir.c_str()) != 0 && errno != EEXIST) {
|
||||
std::cerr << "Failed to create output directory: " << outputDir << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
DFileFindData findData;
|
||||
if (!dbaseFindFirstEntry(dbase, &findData, pattern.c_str())) {
|
||||
std::cerr << "No entries matched pattern: " << pattern << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
int extracted = 0;
|
||||
do {
|
||||
const DBaseEntry& entry = dbase->entries[findData.index];
|
||||
if (!extractEntry(dbase, entry, outputDir, lowerExtractedPaths)) {
|
||||
return 1;
|
||||
}
|
||||
extracted++;
|
||||
} while (dbaseFindNextEntry(dbase, &findData));
|
||||
|
||||
std::cout << "Extracted " << extracted << " entr";
|
||||
std::cout << (extracted == 1 ? "y" : "ies") << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int catCommand(DBase* dbase, const std::vector<std::string>& args)
|
||||
{
|
||||
if (args.size() != 1) {
|
||||
std::cerr << "cat requires exactly one archive entry path\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string entryPath = normalizeDatPath(args[0]);
|
||||
DFile* stream = dfileOpen(dbase, entryPath.c_str(), "rb");
|
||||
if (stream == nullptr) {
|
||||
std::cerr << "Entry not found: " << entryPath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (_setmode(_fileno(stdout), _O_BINARY) == -1) {
|
||||
dfileClose(stream);
|
||||
std::cerr << "Failed to switch stdout to binary mode\n";
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<char> buffer(64 * 1024);
|
||||
long remaining = dfileGetSize(stream);
|
||||
while (remaining > 0) {
|
||||
size_t chunkSize = static_cast<size_t>(std::min<long>(remaining, buffer.size()));
|
||||
size_t bytesRead = dfileRead(buffer.data(), 1, chunkSize, stream);
|
||||
if (bytesRead == 0) {
|
||||
dfileClose(stream);
|
||||
std::cerr << "Failed while reading entry: " << entryPath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (bytesRead != chunkSize) {
|
||||
dfileClose(stream);
|
||||
std::cerr << "Short read while reading entry: " << entryPath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
remaining -= static_cast<long>(bytesRead);
|
||||
|
||||
std::cout.write(buffer.data(), static_cast<std::streamsize>(bytesRead));
|
||||
if (!std::cout) {
|
||||
dfileClose(stream);
|
||||
std::cerr << "Failed while writing to stdout\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::cout) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (dfileClose(stream) != 0) {
|
||||
std::cerr << "Failed to close entry stream cleanly: " << entryPath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int run(const Options& options)
|
||||
{
|
||||
DBase* dbase = dbaseOpen(options.archivePath.c_str());
|
||||
if (dbase == nullptr) {
|
||||
std::cerr << "Failed to open archive: " << options.archivePath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
if (options.command == "list") {
|
||||
std::string pattern = "*";
|
||||
if (!options.args.empty()) {
|
||||
pattern = normalizeDatPath(options.args[0]);
|
||||
}
|
||||
rc = listCommand(dbase, pattern);
|
||||
} else if (options.command == "info") {
|
||||
rc = infoCommand(dbase, options.args);
|
||||
} else if (options.command == "extract") {
|
||||
rc = extractCommand(dbase, options.args, options.lowerExtractedPaths);
|
||||
} else if (options.command == "cat") {
|
||||
rc = catCommand(dbase, options.args);
|
||||
} else {
|
||||
std::cerr << "Unknown command: " << options.command << "\n";
|
||||
printUsage(std::cerr);
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
dbaseClose(dbase);
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace fallout
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
fallout::Options options;
|
||||
if (!fallout::parseOptions(argc, argv, &options)) {
|
||||
fallout::printUsage(std::cerr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
return fallout::run(options);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Unhandled error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user