savegame: save and load current music track and timestamp (#436)

Resolves #419.
This commit is contained in:
walkawayy 2023-08-04 10:23:55 -04:00 committed by Marcin Kurczewski
parent 5197940a6a
commit 04c08b1964
No known key found for this signature in database
GPG key ID: CC65E6FD28CAE42A
17 changed files with 203 additions and 16 deletions

View file

@ -1,4 +1,5 @@
## [Unreleased](https://github.com/rr-/Tomb1Main/compare/stable...develop) - ××××-××-××
- added the current music track and timestamp to the savegame so they now persist on load (#419)
- fixed Natla's gun moving while she is in her semi death state (#878)
- fixed an error message from showing on exiting the game when the gym level is not present in the gameflow (#899)
- fixed the bear pat attack so it does not miss Lara (#450)

View file

@ -367,6 +367,7 @@ Not all options are turned on by default. Refer to `Tomb1Main_ConfigTool.exe` fo
- added music during the credits
- added an option to turn off sound effect pitching
- added an option to use the PlayStation Uzi sound effects
- added the current music track and timestamp to the savegame so they now persist on load
- fixed the sound of collecting a secret killing the music
- fixed audio mixer stopping playing sounds on big explosions
- fixed game audio not muting when game is minimized

View file

@ -231,6 +231,7 @@ bool Config_ReadFromJSON(const char *cfg_data)
READ_BOOL(fix_texture_issues, true);
READ_BOOL(enable_swing_cancel, true);
READ_BOOL(enable_tr2_jumping, false);
READ_BOOL(load_current_music, true);
CLAMP(g_Config.start_lara_hitpoints, 1, LARA_HITPOINTS);
CLAMP(g_Config.fov_value, 30, 255);
@ -415,6 +416,7 @@ bool Config_Write(void)
WRITE_BOOL(fix_texture_issues);
WRITE_BOOL(enable_swing_cancel);
WRITE_BOOL(enable_tr2_jumping);
WRITE_BOOL(load_current_music);
// User settings
WRITE_BOOL(rendering.enable_bilinear_filter);

View file

@ -111,6 +111,7 @@ typedef struct {
bool fix_texture_issues;
bool enable_swing_cancel;
bool enable_tr2_jumping;
bool load_current_music;
struct {
int32_t layout;

View file

@ -133,6 +133,12 @@ bool Game_Start(int32_t level_num, GAMEFLOW_LEVEL_TYPE level_type)
// reset current info to the defaults so that we do not do
// Item_GlobalReplace in the inventory initialization routines too early
Savegame_InitCurrentInfo();
// prevent audio clipping that occurs when level plays a music track,
// and later savegame routines override it while the stream is already
// playing in the background thread
Music_Pause();
if (!Level_Initialise(level_num)) {
return false;
}
@ -140,6 +146,7 @@ bool Game_Start(int32_t level_num, GAMEFLOW_LEVEL_TYPE level_type)
LOG_ERROR("Failed to load save file!");
return false;
}
Music_Unpause();
break;
case GFL_RESTART:

View file

@ -192,3 +192,19 @@ int16_t Music_CurrentTrack(void)
{
return m_Track;
}
int64_t Music_GetTimestamp(int16_t track)
{
if (m_AudioStreamID < 0) {
return -1;
}
return S_Audio_StreamGetTimestamp(m_AudioStreamID);
}
bool Music_SeekTimestamp(int16_t track, int64_t timestamp)
{
if (m_AudioStreamID < 0) {
return false;
}
return S_Audio_StreamSeekTimestamp(m_AudioStreamID, timestamp);
}

View file

@ -38,3 +38,9 @@ void Music_Unpause(void);
// Returns currently playing track. If there is a track playing "over" a looped
// track, returns the "overriding" track number.
int16_t Music_CurrentTrack(void);
// Get timestamp of current stream.
int64_t Music_GetTimestamp(int16_t track);
// Seek to timestamp of current stream.
bool Music_SeekTimestamp(int16_t track, int64_t timestamp);

View file

@ -13,7 +13,7 @@
// creatures, triggers etc., and is what actually sets Lara's health, creatures
// status, triggers, inventory etc.
#define SAVEGAME_CURRENT_VERSION 2
#define SAVEGAME_CURRENT_VERSION 3
typedef enum SAVEGAME_VERSION {
VERSION_LEGACY = -1,

View file

@ -7,6 +7,7 @@
#include "game/items.h"
#include "game/lara.h"
#include "game/lot.h"
#include "game/music.h"
#include "game/room.h"
#include "game/shell.h"
#include "global/const.h"
@ -19,6 +20,7 @@
#include "util.h"
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <zconf.h>
#include <zlib.h>
@ -60,6 +62,7 @@ static bool Savegame_BSON_LoadAmmo(
static bool Savegame_BSON_LoadLOT(struct json_object_s *lot_obj, LOT_INFO *lot);
static bool Savegame_BSON_LoadLara(
struct json_object_s *lara_obj, LARA_INFO *lara);
static bool SaveGame_BSON_LoadCurrentMusic(struct json_object_s *music_obj);
static struct json_array_s *Savegame_BSON_DumpResumeInfo(
RESUME_INFO *game_info);
static struct json_object_s *Savegame_BSON_DumpMisc(GAME_INFO *game_info);
@ -72,6 +75,7 @@ static struct json_object_s *Savegame_BSON_DumpArm(LARA_ARM *arm);
static struct json_object_s *Savegame_BSON_DumpAmmo(AMMO_INFO *ammo);
static struct json_object_s *Savegame_BSON_DumpLOT(LOT_INFO *lot);
static struct json_object_s *Savegame_BSON_DumpLara(LARA_INFO *lara);
static struct json_object_s *SaveGame_BSON_DumpCurrentMusic(void);
static void SaveGame_BSON_SaveRaw(
MYFILE *fp, struct json_value_s *root, int32_t version)
@ -797,6 +801,31 @@ static bool Savegame_BSON_LoadLara(
return true;
}
static bool SaveGame_BSON_LoadCurrentMusic(struct json_object_s *music_obj)
{
if (!g_Config.load_current_music) {
return true;
}
if (!music_obj) {
LOG_WARNING("Malformed save: invalid or missing current music");
return true;
}
int16_t current_track = json_object_get_int(music_obj, "current_track", -1);
int64_t timestamp = json_object_get_int64(music_obj, "timestamp", -1);
if (current_track) {
Music_Play(current_track);
if (!Music_SeekTimestamp(current_track, timestamp)) {
LOG_WARNING(
"Could not load current track %d at timestamp %" PRId64 ".",
current_track, timestamp);
}
}
return true;
}
static struct json_array_s *Savegame_BSON_DumpResumeInfo(
RESUME_INFO *resume_info)
{
@ -1106,6 +1135,20 @@ static struct json_object_s *Savegame_BSON_DumpLara(LARA_INFO *lara)
return lara_obj;
}
static struct json_object_s *SaveGame_BSON_DumpCurrentMusic(void)
{
struct json_object_s *current_music_obj = json_object_new();
json_object_append_int(
current_music_obj, "current_track", Music_CurrentTrack());
int64_t timestamp = 0;
if (Music_CurrentTrack()) {
timestamp = Music_GetTimestamp(Music_CurrentTrack());
}
json_object_append_int64(current_music_obj, "timestamp", timestamp);
return current_music_obj;
}
char *Savegame_BSON_GetSaveFileName(int32_t slot)
{
size_t out_size = snprintf(NULL, 0, g_GameFlow.savegame_fmt_bson, slot) + 1;
@ -1219,6 +1262,11 @@ bool Savegame_BSON_LoadFromFile(MYFILE *fp, GAME_INFO *game_info)
goto cleanup;
}
if (!SaveGame_BSON_LoadCurrentMusic(
json_object_get_object(root_obj, "music"))) {
goto cleanup;
}
ret = true;
cleanup:
@ -1286,6 +1334,8 @@ void Savegame_BSON_SaveToFile(MYFILE *fp, GAME_INFO *game_info)
json_object_append_array(root_obj, "fx", SaveGame_BSON_DumpFx());
json_object_append_object(
root_obj, "lara", Savegame_BSON_DumpLara(&g_Lara));
json_object_append_object(
root_obj, "music", SaveGame_BSON_DumpCurrentMusic());
struct json_value_s *root = json_value_from_object(root_obj);
SaveGame_BSON_SaveRaw(fp, root, SAVEGAME_CURRENT_VERSION);

View file

@ -2,6 +2,7 @@
#include "memory.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -68,6 +69,17 @@ struct json_number_s *json_number_new_int(int number)
return elem;
}
struct json_number_s *json_number_new_int64(int64_t number)
{
size_t size = snprintf(NULL, 0, "%" PRId64, number) + 1;
char *buf = Memory_Alloc(size);
sprintf(buf, "%" PRId64, number);
struct json_number_s *elem = Memory_Alloc(sizeof(struct json_number_s));
elem->number = buf;
elem->number_size = strlen(buf);
return elem;
}
struct json_number_s *json_number_new_double(double number)
{
size_t size = snprintf(NULL, 0, "%f", number) + 1;
@ -317,6 +329,13 @@ void json_object_append_int(
obj, key, json_value_from_number(json_number_new_int(number)));
}
void json_object_append_int64(
struct json_object_s *obj, const char *key, int64_t number)
{
json_object_append(
obj, key, json_value_from_number(json_number_new_int64(number)));
}
void json_object_append_double(
struct json_object_s *obj, const char *key, double number)
{
@ -402,6 +421,17 @@ int json_object_get_int(struct json_object_s *obj, const char *key, int d)
return d;
}
int64_t json_object_get_int64(
struct json_object_s *obj, const char *key, int64_t d)
{
struct json_value_s *value = json_object_get_value(obj, key);
struct json_number_s *num = json_value_as_number(value);
if (num) {
return strtoll(num->number, NULL, 10);
}
return d;
}
double json_object_get_double(
struct json_object_s *obj, const char *key, double d)
{

View file

@ -81,6 +81,7 @@ struct json_value_ex_s {
// numbers
struct json_number_s *json_number_new_int(int number);
struct json_number_s *json_number_new_int64(int64_t number);
struct json_number_s *json_number_new_double(double number);
void json_number_free(struct json_number_s *num);
@ -126,6 +127,8 @@ void json_object_append(
void json_object_append_bool(struct json_object_s *obj, const char *key, int b);
void json_object_append_int(
struct json_object_s *obj, const char *key, int number);
void json_object_append_int64(
struct json_object_s *obj, const char *key, int64_t number);
void json_object_append_double(
struct json_object_s *obj, const char *key, double number);
void json_object_append_string(
@ -141,6 +144,8 @@ struct json_value_s *json_object_get_value(
struct json_object_s *obj, const char *key);
int json_object_get_bool(struct json_object_s *obj, const char *key, int d);
int json_object_get_int(struct json_object_s *obj, const char *key, int d);
int64_t json_object_get_int64(
struct json_object_s *obj, const char *key, int64_t d);
double json_object_get_double(
struct json_object_s *obj, const char *key, double d);
const char *json_object_get_string(

View file

@ -4,6 +4,7 @@
#include <libavutil/samplefmt.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define AUDIO_MAX_SAMPLES 1000
#define AUDIO_MAX_ACTIVE_SAMPLES 50
@ -38,6 +39,8 @@ bool S_Audio_SampleSoundClose(int sound_id);
bool S_Audio_SampleSoundCloseAll(void);
bool S_Audio_SampleSoundSetPan(int sound_id, int pan);
bool S_Audio_SampleSoundSetVolume(int sound_id, int volume);
int64_t S_Audio_StreamGetTimestamp(int sound_id);
bool S_Audio_StreamSeekTimestamp(int sound_id, int64_t timestamp);
#ifdef S_AUDIO_IMPL
#include <libavformat/avformat.h>

View file

@ -31,6 +31,7 @@ typedef struct AUDIO_STREAM_SOUND {
bool is_read_done;
bool is_looped;
float volume;
int64_t timestamp;
void (*finish_callback)(int sound_id, void *user_data);
void *finish_callback_user_data;
@ -58,6 +59,7 @@ static bool S_Audio_StreamSoundDecodeFrame(AUDIO_STREAM_SOUND *stream);
static bool S_Audio_StreamSoundEnqueueFrame(AUDIO_STREAM_SOUND *stream);
static bool S_Audio_StreamSoundInitialiseFromPath(
int sound_id, const char *file_path);
static void S_Audio_StreamSoundClear(AUDIO_STREAM_SOUND *stream);
static bool S_Audio_StreamSoundDecodeFrame(AUDIO_STREAM_SOUND *stream)
{
@ -139,6 +141,7 @@ static bool S_Audio_StreamSoundEnqueueFrame(AUDIO_STREAM_SOUND *stream)
break;
}
stream->timestamp = stream->av.frame->best_effort_timestamp;
av_frame_unref(stream->av.frame);
}
@ -240,7 +243,9 @@ static bool S_Audio_StreamSoundInitialiseFromPath(
stream->is_playing = true;
stream->is_looped = false;
stream->volume = 1.0f;
stream->timestamp = 0;
stream->finish_callback = NULL;
stream->finish_callback_user_data = NULL;
stream->sdl.stream = SDL_NewAudioStream(
sdl_format, sdl_channels, sdl_sample_rate, AUDIO_WORKING_FORMAT,
@ -269,16 +274,23 @@ cleanup:
return ret;
}
static void S_Audio_StreamSoundClear(AUDIO_STREAM_SOUND *stream)
{
stream->is_used = false;
stream->is_playing = false;
stream->is_looped = false;
stream->is_read_done = true;
stream->volume = 0.0f;
stream->timestamp = 0;
stream->sdl.stream = NULL;
stream->finish_callback = NULL;
stream->finish_callback_user_data = NULL;
}
void S_Audio_StreamSoundInit(void)
{
for (int sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS; sound_id++) {
AUDIO_STREAM_SOUND *stream = &m_StreamSounds[sound_id];
stream->is_used = false;
stream->is_playing = false;
stream->is_read_done = true;
stream->volume = 0.0f;
stream->sdl.stream = NULL;
stream->finish_callback = NULL;
S_Audio_StreamSoundClear(&m_StreamSounds[sound_id]);
}
}
@ -382,18 +394,17 @@ bool S_Audio_StreamSoundClose(int sound_id)
if (stream->sdl.stream) {
SDL_FreeAudioStream(stream->sdl.stream);
stream->sdl.stream = NULL;
}
stream->is_read_done = true;
stream->is_used = false;
stream->is_playing = false;
stream->is_looped = false;
stream->volume = 0.0f;
void (*finish_callback)(int, void *) = stream->finish_callback;
void *finish_callback_user_data = stream->finish_callback_user_data;
S_Audio_StreamSoundClear(stream);
SDL_UnlockAudioDevice(g_AudioDeviceID);
if (stream->finish_callback) {
stream->finish_callback(sound_id, stream->finish_callback_user_data);
if (finish_callback) {
finish_callback(sound_id, finish_callback_user_data);
}
return true;
@ -516,3 +527,40 @@ void S_Audio_StreamSoundMix(float *dst_buffer, size_t len)
}
}
}
int64_t S_Audio_StreamGetTimestamp(int sound_id)
{
if (!g_AudioDeviceID || sound_id < 0
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
return -1;
}
if (m_StreamSounds[sound_id].is_playing) {
SDL_LockAudioDevice(g_AudioDeviceID);
AUDIO_STREAM_SOUND *stream = &m_StreamSounds[sound_id];
int64_t timestamp = stream->timestamp;
SDL_UnlockAudioDevice(g_AudioDeviceID);
return timestamp;
}
return -1;
}
bool S_Audio_StreamSeekTimestamp(int sound_id, int64_t timestamp)
{
if (!g_AudioDeviceID || sound_id < 0
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
return false;
}
if (m_StreamSounds[sound_id].is_playing) {
SDL_LockAudioDevice(g_AudioDeviceID);
AUDIO_STREAM_SOUND *stream = &m_StreamSounds[sound_id];
av_seek_frame(
stream->av.format_ctx, sound_id, timestamp, AVSEEK_FLAG_ANY);
SDL_UnlockAudioDevice(g_AudioDeviceID);
return true;
}
return false;
}

View file

@ -279,6 +279,10 @@
"Title": "Screenshot format",
"Description": "Screenshot file format."
},
"load_current_music": {
"Title": "Load music track",
"Description": "Loads the music track that was playing when the game was saved."
},
"enable_music_in_menu": {
"Title": "Enable main menu music",
"Description": "Plays music in the main menu."

View file

@ -195,6 +195,10 @@
"Description": "Permite que los sonidos del juego continúen sonando en la pantalla de inventario.",
"Title": "Habilitar sonidos de juegos en el inventario"
},
"load_current_music": {
"Title": "Load music track",
"Description": "Carga la pista de música que se estaba reproduciendo cuando se guardó el juego."
},
"enable_music_in_menu": {
"Description": "Reproduce música en el menú principal.",
"Title": "Habilitar la música del menú principal"

View file

@ -279,6 +279,10 @@
"Title": "Format des screenshots",
"Description": "Selectionner le format voulu pour les captures d'écran en jeu."
},
"load_current_music": {
"Title": "Load music track",
"Description": "Charge la musique qui était en cours de lecture lors de la sauvegarde."
},
"enable_music_in_menu": {
"Title": "Activer la musique dans le menu principal",
"Description": "Chosir d'activer ou non la musique dans le menu principal."

View file

@ -335,6 +335,11 @@
"ID": "sound",
"Image": "Graphics/graphic6.jpg",
"Properties": [
{
"Field": "load_current_music",
"DataType": "Bool",
"DefaultValue": true
},
{
"Field": "enable_music_in_menu",
"DataType": "Bool",