Compare commits

..
Author SHA1 Message Date
github-actions[bot] 139fc3c440 chore: auto-format with clang-format 2026-04-20 20:41:03 +00:00
phobos2077 90ca28a13c Mod config patching system 2026-04-20 22:30:31 +02:00
Vlad Kandgithub-actions[bot] fbae40e9a8 Move content-related ddraw.ini settings into new DB config, introduce ce.dat (#387)
* Add basic content config structure

* Moved non-file related settings from ddraw.ini to game.cfg

* Various fixes from self review

* Moved remaining worldmap settings to game.cfg

* Migrate character and text related settings

* Update docs

* chore: auto-format with clang-format

* Add hard-coded foce_base.dat to have reliable location to load CE-provided content from

* Move game.cfg to new base mod

* Automatic migration from ddraw.ini

* chore: auto-format with clang-format

* Removed option to disable cities limit fix

* Minor review comment fixes

* Migrate to /data instead of CE dat

* Migrate to local game#patch.cfg and skip fields with default values

- This was if some mod modifies/overwrites game.cfg later, only fields that user cares about will be loaded from user's override, and said mod is less likely to break

* Fix potential issue when "master_patches" is an absolute path and move recursive mkdir into a helper function

* Refactor code and fix some edge cases: data folder not exists when migrating, ddraw.ini doesn't exist

* Rename face_base.dat -> ce.dat

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-20 20:23:13 +00:00
Mike Klaas 4070d29fcb set/remove_script and HOOK_STDPROCEDURE{_END} (#392)
* `set/remove_script` and `HOOK_STDPROCEDURE{_END}`

These were implemented together as I thought HOOK_STDPROCEDURE would be useful for testing set_script.
2026-04-20 19:56:08 +00:00
Mike Klaas dcba8882bf Commandline .dat file viewer (#373) 2026-04-19 15:29:18 -07:00
Mike Klaas 62c2621fef Implement HOOK_ONDEATH (#384)
* Implement HOOK_ONDEATH

Test both combat death and script-triggered death

* move hp and DEAD flag above hook call
2026-04-19 14:24:51 -07:00
29 changed files with 925 additions and 327 deletions
+1
View File
@@ -10,6 +10,7 @@
*.userosscache
*.sln.docstates
.DS_Store
.local-tools/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
-25
View File
@@ -1,25 +0,0 @@
## Further Reading
- Refer to the project README for overall architecture.
## Working With Code
- Only search for code and symbols inside the `src/` directory.
- NEVER search entire code base for certain file or symbol when it's location isn't known, ask first.
- NEVER read entire files unless absolutely necessary for task. Use MCP search instead.
- Prefer CLion MCP for searching code and basic refactoring over raw text tools such as grep.
- Test changes by compiling (separate cc file for small local changes, or via CLion build_project MCP for big changes).
- If game run test is request by prompt, start the game from C:\Games\Fallout2\@CE folder. It always contains last built executable, config file and game assets for full testing.
## Language Standard
- The project targets C++17 (`CMAKE_CXX_STANDARD 17` in `CMakeLists.txt`).
- Prefer C++17 library APIs and semantics when they simplify code. For example, `std::string::data()` is mutable in C++17.
- static global variable need no prefix. exported globals get g*.
- generally prefer camelCase
## Project Structure
- `src/`: All core game logic and engine implementation.
- `third_party/`: Vendored dependencies (zlib, SDL2, etc.).
- `os/`: Platform-specific assets and configuration (icons, plists, etc.).
- `cmake/`: Custom CMake scripts.
- `files/`: Default configuration files and data assets.
- `sfall_testing/`: SSL scripts for testing Sfall-related functionality.
+22
View File
@@ -448,6 +448,28 @@ target_include_directories(${EXECUTABLE_NAME} PRIVATE ${ZLIB_INCLUDE_DIRS})
target_link_libraries(${EXECUTABLE_NAME} ${SDL2_LIBRARIES})
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${SDL2_INCLUDE_DIRS})
add_executable(fallout2-dat
"tools/dat_tool.cc"
"src/dfile.cc"
"src/platform_compat.cc"
)
if(APPLE)
set_target_properties(fallout2-dat PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO"
)
endif()
target_link_libraries(fallout2-dat
fpattern_windows::fpattern_windows
${ZLIB_LIBRARIES}
${SDL2_LIBRARIES}
)
target_include_directories(fallout2-dat PRIVATE "src")
target_include_directories(fallout2-dat PRIVATE ${ZLIB_INCLUDE_DIRS})
target_include_directories(fallout2-dat PRIVATE ${SDL2_INCLUDE_DIRS})
if(APPLE)
if(IOS)
install(TARGETS ${EXECUTABLE_NAME} DESTINATION "Payload")
-3
View File
@@ -119,9 +119,6 @@
"base"
],
"generator": "Xcode",
"cacheVariables": {
"CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY": ""
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
+20
View File
@@ -2,6 +2,26 @@
This file is currently a shell to be filled in over time. For now, it only captures project-specific maintenance notes that are easy to miss.
## `.dat` CLI Tool
There is a small command-line archive tool in this repo for inspecting Fallout `.dat` files using the same reader implementation as the game.
Build it with `make TARGET=fallout2-dat`.
The executable is written to your selected `BUILD_DIR`, so the default path is `out/build/local-debug-arm64/fallout2-dat` on this macOS setup, but other platforms or custom build directories will differ.
The current tool is read-only. Available commands:
1. `./<BUILD_DIR>/fallout2-dat <archive.dat> list [pattern]`
2. `./<BUILD_DIR>/fallout2-dat <archive.dat> info [pattern]`
3. `./<BUILD_DIR>/fallout2-dat <archive.dat> extract [--lower] <output-dir> [pattern]`
4. `./<BUILD_DIR>/fallout2-dat <archive.dat> cat <entry>`
Use `--lower` with `extract` when you want every extracted file and directory name forced to lowercase.
For example, from `/Applications/Fallout2Codex` you can run:
`/Users/klaas/game/fallout2-ce/out/build/local-debug-arm64/fallout2-dat master.dat extract --lower /tmp/fallout2-dat-lower data\\*`
## Updating SDL
SDL is pinned for native builds in `third_party/sdl2/CMakeLists.txt`. Right now, Android also relies on checked-in Java bindings in `os/android/app/src/main/java/org/libsdl/app`, and those bindings must match the SDL version fetched by CMake.
+5 -5
View File
@@ -8,7 +8,7 @@ For now, this covers opcodes/metarules, and hooks. In the future, it will inclu
Settings previously read from `ddraw.ini` have been moved into standard CE config files.
Most settings that control game behavior (premade characters, extra message files, combat tweaks, worldmap, etc.) have been moved into [`<DAT>/config/game.cfg`](files/foce_base.dat/config/game.cfg), which is a content-mod config file intended to be overridden by mods. See that file for the full list with descriptions.
Most settings that control game behavior (premade characters, extra message files, combat tweaks, worldmap, etc.) have been moved into [`<DAT>/config/game.cfg`](files/ce.dat/config/game.cfg), which is a content-mod config file intended to be overridden by mods. See that file for the full list with descriptions.
The following settings were moved into [`fallout2.cfg`](files/fallout2.cfg) instead:
@@ -70,7 +70,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| Interface / Cursor | get/set_cursor_mode | âś… | - |
| Locks | lock_is_jammed<br>unjam_lock<br>set_unjam_locks_time | not implemented | - |
| INI settings | get_ini_setting<br>get_ini_string<br>get_ini_section<br>get_ini_sections<br>get_ini_config<br>get_ini_config_db<br>set_ini_setting | âś… except get_ini_config, get_ini_config_db | `modified_ini` is intentionally omitted as deprecated. |
| Objects and scripts | set_self<br>set_dude_obj<br>real_dude_obj<br>remove_script<br>get/set_script<br>obj_is_carrying_obj<br>loot_obj<br>dialog_obj<br>obj_under_cursor<br>get/set_object_data<br>get/set_flags<br>set_unique_id<br>set_scr_name<br>obj_is_openable<br>get/set_proto_data<br>get_object_ai_data | implemented: set_self, get_script, obj_is_carrying_obj, loot_obj, dialog_obj, obj_under_cursor, get_object_data, get_flags, set_flags, obj_is_openable, get_proto_data, set_proto_data | - |
| 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 | - |
| NPCs | inc_npc_level<br>get_npc_level<br>npc_engine_level_up | not implemented | - |
@@ -86,7 +86,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. |
@@ -104,8 +104,8 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| InvenWield | `HOOK_INVENWIELD` | đźš« | - |
| AdjustFID | `HOOK_ADJUSTFID` | đźš« | - |
| CombatTurn | `HOOK_COMBATTURN` | âś… | - |
| StdProcedure | `HOOK_STDPROCEDURE` | đźš« | Et tu |
| StdProcedureEnd | `HOOK_STDPROCEDURE_END` | đźš« | - |
| StdProcedure | `HOOK_STDPROCEDURE` | âś… | - |
| StdProcedureEnd | `HOOK_STDPROCEDURE_END` | âś… | - |
| CarTravel | `HOOK_CARTRAVEL` | đźš« | - |
| SetGlobalVar | `HOOK_SETGLOBALVAR` | đźš« | - |
| RestTimer | `HOOK_RESTTIMER` | đźš« | Et tu |
@@ -0,0 +1,59 @@
#include "sfall.h"
#include "test_utils.h"
#define TEST_SCRIPT_INDEX (2)
// Depends on Test0.int, which provides a controllable object script helper.
// Put the compiled helper at mods/test_script_override/scripts/Test0.int,
// and add "test_script_override" to mod_order.txt. The helper must live there
// (and not scripts/) because patch000.dat already contains a stock Test0.int and
// scripts/ load before it.
// Without Test0, these 2 tests will fail:
// - "set_script normal hp"
// - "set_script no map_enter hp"
variable testScriptObj := 0;
procedure create_test_object begin
if (testScriptObj != 0) then return;
testScriptObj := create_object_sid(obj_pid(dude_obj), tile_num_in_direction(tile_num(dude_obj), has_trait(TRAIT_OBJECT, dude_obj, OBJECT_CUR_ROT), 3), elevation(dude_obj), -1);
end
procedure stdprocedure_hook begin
if (get_sfall_arg_at(0) != 15) then return;
if (get_sfall_arg_at(3)) then begin
set_sfall_global("T0HAFTR1", 1);
end else begin
set_sfall_global("T0HBEFO1", 1);
end
end
procedure exercise_set_script(variable scriptId, variable expectedHp, variable expectedBeforeHook, variable expectedAfterHook, variable desc) begin
set_sfall_global("T0MAPEN1", 0);
set_sfall_global("T0HBEFO1", 0);
set_sfall_global("T0HAFTR1", 0);
set_script(testScriptObj, scriptId);
call assertEquals(desc + " get_script", get_script(testScriptObj), TEST_SCRIPT_INDEX);
call assertEquals(desc + " map_enter global", get_sfall_global_int("T0MAPEN1"), expectedHp);
call assertEquals(desc + " stdprocedure hook before", get_sfall_global_int("T0HBEFO1"), expectedBeforeHook);
call assertEquals(desc + " stdprocedure hook after", get_sfall_global_int("T0HAFTR1"), expectedAfterHook);
remove_script(testScriptObj);
call assertEquals(desc + " remove_script clears get_script", get_script(testScriptObj), 0);
end
procedure start begin
if (not game_loaded) then return;
display_msg("Testing set_script/remove_script/get_script...");
register_hook_proc(HOOK_STDPROCEDURE, stdprocedure_hook);
register_hook_proc(HOOK_STDPROCEDURE_END, stdprocedure_hook);
call create_test_object;
call exercise_set_script(TEST_SCRIPT_INDEX, 22, 1, 1, "set_script normal");
call exercise_set_script(TEST_SCRIPT_INDEX bwor 0x80000000, 11, 0, 0, "set_script no map_enter");
destroy_object(testScriptObj);
testScriptObj := 0;
call report_test_results("script_manipulation");
end
+52
View File
@@ -0,0 +1,52 @@
#include "sfall.h"
#include "dik.h"
#include "define_lite.h"
#define TEST_KEY (DIK_J)
procedure ondeath_handler begin
variable
dead_critter := get_sfall_arg_at(0),
critter_name := "<null>",
critter_pid := -1;
if (dead_critter) then begin
critter_name := obj_name(dead_critter);
critter_pid := obj_pid(dead_critter);
end
display_msg(string_format2("ondeath critter=%s pid=%d", critter_name, critter_pid));
end
procedure keypress_handler begin
variable
pressed := get_sfall_arg_at(0),
key := get_sfall_arg_at(1),
target;
if (not pressed) then return;
if (key != TEST_KEY) then return;
target := obj_under_cursor(true, false);
if (target == 0) then begin
display_msg("ondeath test: no critter under cursor");
return;
end
if (target == dude_obj) then begin
display_msg("ondeath test: refusing to kill dude");
return;
end
display_msg(string_format1("ondeath test: killing %s", obj_name(target)));
critter_dmg(target, 9999, DMG_BYPASS_ARMOR);
end
procedure start begin
if (not game_loaded) then return;
display_msg("ondeath manual test ready: hover a critter and press J");
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
register_hook_proc(HOOK_ONDEATH, ondeath_handler);
end
+1
View File
@@ -4936,6 +4936,7 @@ static void _damage_object(Object* a1, int damage, bool animated, int a4, Object
}
partyMemberRemove(a1);
scriptHooks_OnDeath(a1);
}
}
-46
View File
@@ -132,7 +132,6 @@ static bool aiCanUseItem(Object* obj, Object* a2);
static Object* _ai_search_environ(Object* critter, int itemType);
static Object* _ai_retrieve_object(Object* critter, Object* item);
static int _ai_pick_hit_mode(Object* attacker, Object* weapon, Object* defender);
static bool _aiIsMeleeThreat(Object* attacker, Object* defender);
static int _ai_move_steps_closer(Object* a1, Object* a2, int actionPoints, bool taunt);
static int _ai_move_closer(Object* a1, Object* a2, bool taunt);
static int _cai_retargetTileFromFriendlyFire(Object* source, Object* target, int* tilePtr);
@@ -2352,28 +2351,6 @@ static int _ai_pick_hit_mode(Object* attacker, Object* weapon, Object* defender)
return HIT_MODE_RIGHT_WEAPON_PRIMARY;
}
// Returns true if the defender is primarily a melee threat.
static bool _aiIsMeleeThreat(Object* attacker, Object* defender)
{
if (defender == nullptr) {
return false;
}
Object* weapon = critterGetItem2(defender);
if (weapon != nullptr && itemGetType(weapon) == ITEM_TYPE_WEAPON) {
int attackType = weaponGetAttackTypeForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY);
if (attackType == ATTACK_TYPE_RANGED || attackType == ATTACK_TYPE_THROW) {
return false;
}
if (attackType == ATTACK_TYPE_MELEE || attackType == ATTACK_TYPE_UNARMED) {
return true;
}
}
return true;
}
// 0x429FC8
static int _ai_move_steps_closer(Object* critter, Object* target, int actionPoints, bool taunt)
{
@@ -2744,29 +2721,6 @@ static int _ai_try_attack(Object* attacker, Object* defender)
_ai_switch_weapons(attacker, &hitMode, &weapon, defender);
}
AiPacket* ai = aiGetPacket(attacker);
if (weapon != nullptr && itemGetType(weapon) == ITEM_TYPE_WEAPON) {
int attackType = weaponGetAttackTypeForHitMode(weapon, HIT_MODE_RIGHT_WEAPON_PRIMARY);
if (attackType == ATTACK_TYPE_RANGED
&& ai->best_weapon != BEST_WEAPON_MELEE
&& ai->best_weapon != BEST_WEAPON_MELEE_OVER_RANGED
&& _aiIsMeleeThreat(attacker, defender)
&& objectGetDistanceBetween(attacker, defender) <= 2) {
int attackCost = weaponGetActionPointCost(attacker, hitMode, false);
int moveCost = 2;
if (attacker->data.critter.combat.ap > attackCost + moveCost) {
if (_ai_move_away(attacker, defender, 2) == -1) {
}
weapon = critterGetItem2(attacker);
if (weapon != nullptr && itemGetType(weapon) != ITEM_TYPE_WEAPON) {
weapon = nullptr;
}
hitMode = _ai_pick_hit_mode(attacker, weapon, defender);
}
}
}
unsigned char rotations[800];
Object* ammo = nullptr;
+20
View File
@@ -331,6 +331,26 @@ 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,9 +23,6 @@ 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()
+5 -3
View File
@@ -25,6 +25,7 @@
#include "random.h"
#include "reaction.h"
#include "scripts.h"
#include "sfall_script_hooks.h"
#include "skill.h"
#include "stat.h"
#include "tile.h"
@@ -825,7 +826,11 @@ void critterKill(Object* critter, int anim, bool refreshRect)
int elevation = critter->elevation;
critter->data.critter.hp = 0;
critter->data.critter.combat.results |= DAM_DEAD;
partyMemberRemove(critter);
scriptHooks_OnDeath(critter);
// NOTE: Original code uses goto to jump out from nested conditions below.
bool shouldChangeFid = false;
@@ -893,9 +898,6 @@ void critterKill(Object* critter, int anim, bool refreshRect)
_obj_turn_off_light(critter, &tempRect);
rectUnion(&updatedRect, &tempRect, &updatedRect);
critter->data.critter.hp = 0;
critter->data.critter.combat.results |= DAM_DEAD;
if (critter->sid != -1) {
scriptRemove(critter->sid);
critter->sid = -1;
+5 -1
View File
@@ -88,7 +88,7 @@ static void showHelp();
static int gameDbInit();
static void showSplash();
inline constexpr char kBaseModPath[] = "foce_base.dat";
inline constexpr char kBaseModPath[] = "ce.dat";
// 0x501C9C
static char _aGame_0[] = "game\\";
@@ -1381,6 +1381,10 @@ static int gameDbInit()
if (*patch_file_name == '\0') {
patch_file_name = nullptr;
}
// Try to ensure that patches dir exists early. This is needed for auto-generated game.cfg later.
if (patch_file_name != nullptr) {
compat_mkdir_recursive(patch_file_name);
}
int master_db_handle = dbOpen(main_file_name, patch_file_name);
if (master_db_handle == -1) {
+15 -10
View File
@@ -218,19 +218,17 @@ static bool contentConfigMigrateFromSfall(Config* sfallConfig, const char* conte
assert(sfallConfig != nullptr && contentConfigFilePath != nullptr);
// Skip if a local game.cfg already exists (already migrated or user-managed).
FILE* existing = compat_fopen(contentConfigFilePath, "rt");
if (existing != nullptr) {
fclose(existing);
if (compat_file_exists(contentConfigFilePath)) {
return false;
}
// Migrate start year/month/day only when explicitly set (not the sfall -1 sentinel).
bool migrated = false;
Config migratedConfig;
if (!configInit(&migratedConfig)) {
return false;
}
bool migrated = false;
// Migrate start year/month/day only when explicitly set (not the sfall -1 sentinel).
auto migrateStartInt = [&](const char* sfallKey, const char* targetKey, int defaultValue) {
int value;
if (configGetInt(sfallConfig, "Misc", sfallKey, &value) && value >= 0 && value != defaultValue) {
@@ -257,7 +255,12 @@ static bool contentConfigMigrateFromSfall(Config* sfallConfig, const char* conte
if (migrated) {
// Ensure all directory components exist before writing.
compat_mkdir_recursive(contentConfigFilePath);
char drive[COMPAT_MAX_DRIVE];
char dirPart[COMPAT_MAX_DIR];
char pathWithoutFile[COMPAT_MAX_PATH];
compat_splitpath(contentConfigFilePath, drive, dirPart, nullptr, nullptr);
compat_makepath(pathWithoutFile, drive, dirPart, nullptr, nullptr);
compat_mkdir_recursive(pathWithoutFile);
if (!configWrite(&migratedConfig, contentConfigFilePath, false)) {
debugPrint("Failed to write migrated settings to %s!\n", contentConfigFilePath);
@@ -270,15 +273,17 @@ static bool contentConfigMigrateFromSfall(Config* sfallConfig, const char* conte
void contentConfigTryMigrateFromSfall(const char* contentConfigPath)
{
if (!gSfallConfig.isInitialized() || gSfallConfig.entriesLength == 0) {
// Nothing to migrate.
return;
}
const auto& masterPatches = settings.system.master_patches_path;
if (masterPatches.empty()) {
debugPrint("Failed to migrate from ddraw.ini: no master_patches is set.\n");
return;
}
FILE* baseAsDat = compat_fopen(masterPatches.c_str(), "rb");
if (baseAsDat != nullptr) {
// Master patches is pointing to a dat file. This shouldn't normally happen, so don't migrate in this case.
fclose(baseAsDat);
if (compat_file_exists(masterPatches.c_str())) {
// Master patches is pointing to a file instead of a folder. This shouldn't normally happen, so don't migrate in this case.
return;
}
char contentCfgPath[COMPAT_MAX_PATH];
+1 -1
View File
@@ -287,7 +287,7 @@ typedef struct Object {
int outline; // obj_outline
int sid; // obj_sid
Object* owner;
int scriptIndex;
int scriptIndex; // TODO: remove
} Object;
typedef struct ObjectListNode {
+17 -7
View File
@@ -218,24 +218,34 @@ int compat_mkdir(const char* path)
int compat_mkdir_recursive(const char* path)
{
char drive[COMPAT_MAX_DRIVE];
char dirPart[COMPAT_MAX_DIR];
compat_splitpath(path, drive, dirPart, nullptr, nullptr);
compat_splitpath(path, drive, nullptr, nullptr, nullptr);
char dir[COMPAT_MAX_PATH];
compat_makepath(dir, drive, dirPart, nullptr, nullptr);
char pathCopy[COMPAT_MAX_PATH];
strcpy(pathCopy, path);
// Skip drive root (e.g. "C:\\" or leading "/") to avoid mkdir("") or mkdir("C:").
char* sep = dir + strlen(drive);
char* sep = pathCopy + strlen(drive);
if (*sep == '\\' || *sep == '/') sep++;
for (; *sep != '\0'; sep++) {
if (*sep == '\\' || *sep == '/') {
char saved = *sep;
*sep = '\0';
compat_mkdir(dir);
if (compat_mkdir(pathCopy) < 0) {
break;
}
*sep = saved;
}
}
return compat_mkdir(dir);
return compat_mkdir(path);
}
bool compat_file_exists(const char* filePath)
{
FILE* file = compat_fopen(filePath, "rb");
if (file == nullptr) return false;
fclose(file);
return true;
}
unsigned int compat_timeGetTime()
+1
View File
@@ -33,6 +33,7 @@ long compat_tell(int fileHandle);
long compat_filelength(int fd);
int compat_mkdir(const char* path);
int compat_mkdir_recursive(const char* path);
bool compat_file_exists(const char* filePath);
unsigned int compat_timeGetTime();
FILE* compat_fopen(const char* path, const char* mode);
gzFile compat_gzopen(const char* path, const char* mode);
+1 -1
View File
@@ -130,7 +130,7 @@ int objectSetScriptFromProto(Object* object, int* sidPtr)
return 0;
}
// 0x49AAC0
// 0x49AAC0 obj_new_sid_inst
int objectSetScript(Object* obj, int scriptType, int scriptIndex)
{
if (scriptIndex == -1) {

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