From 344f4e375e41dc733ef8daeb408e9a8acb84e0eb Mon Sep 17 00:00:00 2001 From: Mike Klaas Date: Wed, 22 Jul 2026 22:54:47 -0700 Subject: [PATCH] [Et tu] Movie overrides Including movie1-movie32, mark_movie_played, set_movie_path Verified that Fallout 1 movies play properly with this PR --- SFALL_COMPATIBILITY.md | 3 +- files/ce.dat/config/game.cfg | 35 +++++++++++ src/game_config_migration.cc | 57 +++++++++++++++++ src/game_movie.cc | 116 ++++++++++++++++++++++++++++++----- src/game_movie.h | 4 ++ src/interpreter_extra.cc | 6 +- src/movie.cc | 12 ++++ src/pipboy.cc | 2 +- src/sfall_opcodes.cc | 25 ++++++++ 9 files changed, 242 insertions(+), 18 deletions(-) diff --git a/SFALL_COMPATIBILITY.md b/SFALL_COMPATIBILITY.md index 33d225eb..7d1db364 100644 --- a/SFALL_COMPATIBILITY.md +++ b/SFALL_COMPATIBILITY.md @@ -43,6 +43,7 @@ The following settings were moved into [`/config/game.cfg`](files/ce.dat/co | ddraw.ini section | ddraw.ini key | game.cfg section | game.cfg key | | --- | --- | --- | --- | | `Misc` | `StartGDialogFix` | `dialog` | `start_gdialog_fix` | +| `Misc` | `Movie1` - `Movie32` | `movies` | `movie1` - `movie32` | ## Opcodes / Metarules @@ -89,7 +90,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/) | Locks | lock_is_jammed
unjam_lock
set_unjam_locks_time | not implemented | - | | INI settings | get_ini_setting
get_ini_string
get_ini_section
get_ini_sections
get_ini_config
get_ini_config_db
set_ini_setting | ✅ | `modified_ini` is intentionally omitted as deprecated. | | Objects and scripts | set_self
set_dude_obj
real_dude_obj
remove_script
get/set_script
obj_is_carrying_obj
loot_obj
dialog_obj
obj_under_cursor
get/set_object_data
get/set_flags
set_unique_id
set_scr_name
obj_is_openable
get/set_proto_data
get_object_ai_data | ✅ except set_dude_obj, set_scr_name | `get/set_object_data` maps supported object and `C_ATTACK_*` offsets instead of exposing arbitrary raw memory offsets. | -| Other / Game management | set_movie_path
stop/resume_game
mark_movie_played
game_loaded
get_game_mode
get_uptime
signal_close_game | implemented: game_loaded, get_game_mode, get_uptime, signal_close_game | - | +| Other / Game management | set_movie_path
stop/resume_game
mark_movie_played
game_loaded
get_game_mode
get_uptime
signal_close_game | ✅ except stop/resume_game | - | | Gameplay tweaks | set_pickpocket_max
set_hit_chance_max
set_xp_mod
set_critter_hit_chance_mod
set_base_hit_chance_mod
set_hp_per_level_mod
gdialog_get_barter_mod
get/set_unspent_ap_bonus
get/set_unspent_ap_perk_bonus
set_base_pickpocket_mod
set_critter_pickpocket_mod
get/set_inven_ap_cost
set_drugs_data
get_kill_counter
mod_kill_counter
set_pipboy_available | implemented: gdialog_get_barter_mod, get/set_unspent_ap{_perk}_bonus, get/set_inven_ap_cost | - | | NPCs | inc_npc_level
get_npc_level
npc_engine_level_up | not implemented | - | | Hero Appearance | set_dm/df_model
hero_select_win
set_hero_race
set_hero_style | not implemented | - | diff --git a/files/ce.dat/config/game.cfg b/files/ce.dat/config/game.cfg index 813e6bae..597dea78 100644 --- a/files/ce.dat/config/game.cfg +++ b/files/ce.dat/config/game.cfg @@ -60,6 +60,41 @@ credits_offset_x=0 credits_offset_y=0 [movies] +; Overrides for game movie file names in art\cuts\. These correspond to sfall's Movie1 through Movie32 settings. +; Movie IDs used by scripts are 0-indexed, so movie1 is script movie ID 0. +movie1=iplogo.mve +movie2=intro.mve +movie3=elder.mve +movie4=vsuit.mve +movie5=afailed.mve +movie6=adestroy.mve +movie7=car.mve +movie8=cartucci.mve +movie9=timeout.mve +movie10=tanker.mve +movie11=enclave.mve +movie12=derrick.mve +movie13=artimer1.mve +movie14=artimer2.mve +movie15=artimer3.mve +movie16=artimer4.mve +movie17=credits.mve +; Additional script-playable movies. +movie18= +movie19= +movie20= +movie21= +movie22= +movie23= +movie24= +movie25= +movie26= +movie27= +movie28= +movie29= +movie30= +movie31= +movie32= ; Number of days after game start when each Hakunin dream sequence becomes available. artimer1=90 artimer2=180 diff --git a/src/game_config_migration.cc b/src/game_config_migration.cc index a363e4ef..839ed903 100644 --- a/src/game_config_migration.cc +++ b/src/game_config_migration.cc @@ -7,6 +7,7 @@ #include "content_config.h" #include "debug.h" #include "game_config.h" +#include "game_movie.h" #include "platform_compat.h" #include "settings.h" #include "sfall_config.h" @@ -237,6 +238,58 @@ namespace { { kSfallMisc, "ExtraGameMsgFileList", CONTENT_CONFIG_TEXT_SECTION, "extra_msg_file_list" }, }; + constexpr const char* kSfallDefaultMovies[MOVIE_COUNT] = { + "iplogo.mve", + "intro.mve", + "elder.mve", + "vsuit.mve", + "afailed.mve", + "adestroy.mve", + "car.mve", + "cartucci.mve", + "timeout.mve", + "tanker.mve", + "enclave.mve", + "derrick.mve", + "artimer1.mve", + "artimer2.mve", + "artimer3.mve", + "artimer4.mve", + "credits.mve", + }; + + static bool contentConfigMigrateSfallMovieOverrides(Config* sfallConfig, Config* migratedConfig) + { + assert(sfallConfig != nullptr && migratedConfig != nullptr); + + bool migrated = false; + for (int index = 0; index < GAME_MOVIE_MAX_COUNT; index++) { + char sfallKey[16]; + snprintf(sfallKey, sizeof(sfallKey), "Movie%d", index + 1); + + char* value; + if (!configGetString(sfallConfig, kSfallMisc, sfallKey, &value) || value[0] == '\0') { + continue; + } + + if (index < MOVIE_COUNT && strcmp(value, kSfallDefaultMovies[index]) == 0) { + continue; + } + + char targetKey[16]; + snprintf(targetKey, sizeof(targetKey), "movie%d", index + 1); + + if (gameConfigHasKey(migratedConfig, CONTENT_CONFIG_MOVIES_SECTION, targetKey)) { + continue; + } + + configSetString(migratedConfig, CONTENT_CONFIG_MOVIES_SECTION, targetKey, value); + migrated = true; + } + + return migrated; + } + } // anonymous namespace // Migrate sfall settings from ddraw.ini to game.cfg. @@ -283,6 +336,10 @@ static bool contentConfigMigrateFromSfall(Config* sfallConfig, const char* conte } } + if (contentConfigMigrateSfallMovieOverrides(sfallConfig, &migratedConfig)) { + migrated = true; + } + if (migrated) { // Ensure all directory components exist before writing. char drive[COMPAT_MAX_DRIVE]; diff --git a/src/game_movie.cc b/src/game_movie.cc index 5f500b81..0e0f52df 100644 --- a/src/game_movie.cc +++ b/src/game_movie.cc @@ -1,9 +1,13 @@ #include "game_movie.h" +#include +#include #include #include +#include #include "color.h" +#include "content_config.h" #include "cycle.h" #include "debug.h" #include "game.h" @@ -24,12 +28,16 @@ namespace fallout { static char* gameMovieBuildSubtitlesFilePath(char* movieFilePath); +static bool gameMovieFindFilePath(char* movieFilePath, size_t movieFilePathSize, const char* movieFileName); +static bool gameMovieFindFilePathInDir(char* movieFilePath, size_t movieFilePathSize, const char* dir, const char* movieFileName); +static void gameMovieInitFileNames(); +static void gameMovieLoadConfigFileNames(); // 0x50352A static const float flt_50352A = 0.032258064f; // 0x518DA0 movie_list -static const char* gMovieFileNames[MOVIE_COUNT] = { +static const char* gMovieDefaultFileNames[MOVIE_COUNT] = { "iplogo.mve", "intro.mve", "elder.mve", @@ -49,6 +57,8 @@ static const char* gMovieFileNames[MOVIE_COUNT] = { "credits.mve", }; +static std::array gMovieFileNames; + // 0x518DE4 subtitlePalList static const char* gMoviePaletteFilePaths[MOVIE_COUNT] = { nullptr, @@ -95,6 +105,9 @@ int gameMoviesInit() movieSetBuildSubtitleFilePathProc(gameMovieBuildSubtitlesFilePath); + gameMovieInitFileNames(); + gameMovieLoadConfigFileNames(); + memset(gGameMoviesSeen, 0, sizeof(gGameMoviesSeen)); gGameMovieIsPlaying = false; @@ -136,28 +149,24 @@ int gameMoviesSave(File* stream) // 0x44E690 gmovie_play int gameMoviePlay(int movie, int flags) { + if (movie < 0 || movie >= GAME_MOVIE_MAX_COUNT || gMovieFileNames[movie].empty()) { + debugPrint("\ngmovie_play() - Error: Invalid movie %d\n", movie); + return -1; + } + gGameMovieIsPlaying = true; - const char* movieFileName = gMovieFileNames[movie]; + const char* movieFileName = gMovieFileNames[movie].c_str(); debugPrint("\nPlaying movie: %s\n", movieFileName); const char* language = settings.system.language.c_str(); char movieFilePath[COMPAT_MAX_PATH]; - int movieFileSize; bool movieFound = false; - if (compat_stricmp(language, ENGLISH) != 0) { - snprintf(movieFilePath, sizeof(movieFilePath), "art\\%s\\cuts\\%s", language, gMovieFileNames[movie]); - movieFound = dbGetFileSize(movieFilePath, &movieFileSize) == 0; - } + movieFound = gameMovieFindFilePath(movieFilePath, sizeof(movieFilePath), movieFileName); if (!movieFound) { - snprintf(movieFilePath, sizeof(movieFilePath), "art\\cuts\\%s", gMovieFileNames[movie]); - movieFound = dbGetFileSize(movieFilePath, &movieFileSize) == 0; - } - - if (!movieFound) { - debugPrint("\ngmovie_play() - Error: Unable to open %s\n", gMovieFileNames[movie]); + debugPrint("\ngmovie_play() - Error: Unable to open %s\n", movieFileName); gGameMovieIsPlaying = false; return -1; } @@ -205,7 +214,7 @@ int gameMoviePlay(int movie, int flags) int oldFont; if (subtitlesEnabled) { const char* subtitlesPaletteFilePath; - if (gMoviePaletteFilePaths[movie] != nullptr) { + if (movie < MOVIE_COUNT && gMoviePaletteFilePaths[movie] != nullptr) { subtitlesPaletteFilePath = gMoviePaletteFilePaths[movie]; } else { subtitlesPaletteFilePath = "art\\cuts\\subtitle.pal"; @@ -264,7 +273,7 @@ int gameMoviePlay(int movie, int flags) _movieUpdate(); paletteSetEntries(gPaletteBlack); - gGameMoviesSeen[movie] = 1; + gameMovieMarkSeen(movie); colorCycleEnable(); @@ -308,6 +317,23 @@ int gameMoviePlay(int movie, int flags) return 0; } +bool gameMovieSetPath(int movie, const char* fileName) +{ + if (movie < 0 || movie >= GAME_MOVIE_MAX_COUNT || fileName == nullptr) { + return false; + } + + gMovieFileNames[movie] = fileName; + return true; +} + +void gameMovieMarkSeen(int movie) +{ + if (movie >= 0 && movie < MOVIE_COUNT) { + gGameMoviesSeen[movie] = 1; + } +} + // 0x44EAE4 gmPaletteFinish void gameMovieFadeOut() { @@ -320,6 +346,10 @@ void gameMovieFadeOut() // 0x44EB04 gmovie_has_been_played bool gameMovieIsSeen(int movie) { + if (movie < 0 || movie >= MOVIE_COUNT) { + return false; + } + return gGameMoviesSeen[movie] == 1; } @@ -351,4 +381,60 @@ static char* gameMovieBuildSubtitlesFilePath(char* movieFilePath) return gGameMovieSubtitlesFilePath; } +static bool gameMovieFindFilePath(char* movieFilePath, size_t movieFilePathSize, const char* movieFileName) +{ + assert(movieFilePath != nullptr); + assert(movieFileName != nullptr); + + const char* language = settings.system.language.c_str(); + if (compat_stricmp(language, ENGLISH) != 0) { + char localizedDir[COMPAT_MAX_PATH]; + snprintf(localizedDir, sizeof(localizedDir), "art\\%s\\cuts", language); + if (gameMovieFindFilePathInDir(movieFilePath, movieFilePathSize, localizedDir, movieFileName)) { + return true; + } + } + + return gameMovieFindFilePathInDir(movieFilePath, movieFilePathSize, "art\\cuts", movieFileName); +} + +static bool gameMovieFindFilePathInDir(char* movieFilePath, size_t movieFilePathSize, const char* dir, const char* movieFileName) +{ + char localMovieFilePath[COMPAT_MAX_PATH]; + snprintf(localMovieFilePath, sizeof(localMovieFilePath), ".\\%s\\%s", dir, movieFileName); + if (compat_file_exists(localMovieFilePath)) { + snprintf(movieFilePath, movieFilePathSize, "%s\\%s", dir, movieFileName); + return true; + } + + snprintf(movieFilePath, movieFilePathSize, "%s\\%s", dir, movieFileName); + + int movieFileSize; + return dbGetFileSize(movieFilePath, &movieFileSize) == 0; +} + +static void gameMovieInitFileNames() +{ + for (auto& fileName : gMovieFileNames) { + fileName.clear(); + } + + for (int index = 0; index < MOVIE_COUNT; index++) { + gMovieFileNames[index] = gMovieDefaultFileNames[index]; + } +} + +static void gameMovieLoadConfigFileNames() +{ + char key[16]; + for (int index = 0; index < GAME_MOVIE_MAX_COUNT; index++) { + snprintf(key, sizeof(key), "movie%d", index + 1); + + char* fileName; + if (configGetString(&gContentConfig, CONTENT_CONFIG_MOVIES_SECTION, key, &fileName) && fileName[0] != '\0') { + gameMovieSetPath(index, fileName); + } + } +} + } // namespace fallout diff --git a/src/game_movie.h b/src/game_movie.h index b448e4ed..06cb5599 100644 --- a/src/game_movie.h +++ b/src/game_movie.h @@ -33,11 +33,15 @@ typedef enum GameMovie { MOVIE_COUNT, } GameMovie; +constexpr int GAME_MOVIE_MAX_COUNT = 32; + int gameMoviesInit(); void gameMoviesReset(); int gameMoviesLoad(File* stream); int gameMoviesSave(File* stream); int gameMoviePlay(int movie, int flags); +bool gameMovieSetPath(int movie, const char* fileName); +void gameMovieMarkSeen(int movie); void gameMovieFadeOut(); bool gameMovieIsSeen(int movie); bool gameMovieIsPlaying(); diff --git a/src/interpreter_extra.cc b/src/interpreter_extra.cc index e2da2571..75a2c95d 100644 --- a/src/interpreter_extra.cc +++ b/src/interpreter_extra.cc @@ -3613,6 +3613,10 @@ static void opPlayGameMovie(Program* program) program->flags |= PROGRAM_FLAG_CHILD_CALL; int movie = programStackPopInteger(program); + int movieFlags = GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC; + if (movie >= 0 && movie < MOVIE_COUNT) { + movieFlags = flags[movie]; + } // CE: Disable map updates. Needed to stop animation of objects (dude in // particular) when playing movies (the problem can be seen as visual @@ -3621,7 +3625,7 @@ static void opPlayGameMovie(Program* program) gameDialogDisable(); - if (gameMoviePlay(movie, flags[movie]) == -1) { + if (gameMoviePlay(movie, movieFlags) == -1) { debugPrint("\nError playing movie %d!", movie); } diff --git a/src/movie.cc b/src/movie.cc index 6f7a243c..47308799 100644 --- a/src/movie.cc +++ b/src/movie.cc @@ -536,11 +536,23 @@ void movieSetPaletteProc(MovieSetPaletteProc* proc) // 0x48731C openFile static File* movieOpen(char* filePath) { + char looseFilePath[COMPAT_MAX_PATH]; + snprintf(looseFilePath, sizeof(looseFilePath), ".\\%s", filePath); + + if (compat_file_exists(looseFilePath)) { + gMovieFileStream = fileOpen(looseFilePath, "rb"); + if (gMovieFileStream != nullptr) { + debugPrint("Movie file path: %s\n", looseFilePath); + return gMovieFileStream; + } + } + gMovieFileStream = fileOpen(filePath, "rb"); if (gMovieFileStream == nullptr) { debugPrint("Couldn't find movie file %s\n", filePath); return nullptr; } + debugPrint("Movie file path: %s\n", filePath); return gMovieFileStream; } diff --git a/src/pipboy.cc b/src/pipboy.cc index b9a67e78..47c51d45 100644 --- a/src/pipboy.cc +++ b/src/pipboy.cc @@ -1947,7 +1947,7 @@ static void pipboyHandleVideoArchive(int a1) } } - if (movie <= MOVIE_COUNT) { + if (movie < MOVIE_COUNT) { gameMoviePlay(movie, GAME_MOVIE_FADE_IN | GAME_MOVIE_FADE_OUT | GAME_MOVIE_PAUSE_MUSIC); } else { debugPrint("\n ** Selected movie not found in list! **\n"); diff --git a/src/sfall_opcodes.cc b/src/sfall_opcodes.cc index c8ecc7f5..e82eebd2 100644 --- a/src/sfall_opcodes.cc +++ b/src/sfall_opcodes.cc @@ -14,6 +14,7 @@ #include "debug.h" #include "game.h" #include "game_dialog.h" +#include "game_movie.h" #include "input.h" #include "interface.h" #include "interpreter.h" @@ -211,6 +212,28 @@ static void op_get_year(Program* program) programStackPushInteger(program, year); } +static void op_set_movie_path(Program* program) +{ + int movie = programStackPopInteger(program); + const char* fileName = programStackPopString(program); + if (movie < 0 || movie >= GAME_MOVIE_MAX_COUNT || fileName == nullptr) { + return; + } + + if (strlen(fileName) > 64) { + programPrintError("set_movie_path: filename exceeds 64 characters"); + return; + } + + gameMovieSetPath(movie, fileName); +} + +static void op_mark_movie_played(Program* program) +{ + int movie = programStackPopInteger(program); + gameMovieMarkSeen(movie); +} + // game_loaded static void op_game_loaded(Program* program) { @@ -1962,6 +1985,7 @@ void sfallOpcodesInit() // 0x8175 - void set_dm_model(string name) // 0x8176 - void set_df_model(string name) // 0x8177 - void set_movie_path(string filename, int movieid) + interpreterRegisterOpcode(0x8177, op_set_movie_path); // 0x8178 - void set_perk_image(int perkID, int value) // 0x8179 - void set_perk_ranks(int perkID, int value) @@ -2292,6 +2316,7 @@ void sfallOpcodesInit() // 0x823f - void disable_aimed_shots(int pid) // 0x8240 - void mark_movie_played(int id) + interpreterRegisterOpcode(0x8240, op_mark_movie_played); // 0x8248 - object get_last_target(object critter) // 0x8249 - object get_last_attacker(object critter)