Compare commits

...
Author SHA1 Message Date
Mike Klaas 344f4e375e [Et tu] Movie overrides
Including movie1-movie32, mark_movie_played, set_movie_path

Verified that Fallout 1 movies play properly with this PR
2026-07-22 22:54:47 -07:00
9 changed files with 242 additions and 18 deletions
+2 -1
View File
@@ -43,6 +43,7 @@ The following settings were moved into [`<DAT>/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<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. |
| 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 | âś… 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<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 | - |
| 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 | âś… except stop/resume_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 Appearance | set_dm/df_model<br>hero_select_win<br>set_hero_race<br>set_hero_style | not implemented | - |
+35
View File
@@ -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
+57
View File
@@ -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];
+101 -15
View File
@@ -1,9 +1,13 @@
#include "game_movie.h"
#include <array>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <string>
#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<std::string, GAME_MOVIE_MAX_COUNT> 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
+4
View File
@@ -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();
+5 -1
View File
@@ -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);
}
+12
View File
@@ -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;
}
+1 -1
View File
@@ -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");
+25
View File
@@ -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)