mirror of
https://github.com/LostArtefacts/TRX.git
synced 2025-04-28 20:58:07 +03:00
savegame: save and load current music track and timestamp (#436)
Resolves #419.
This commit is contained in:
parent
5197940a6a
commit
04c08b1964
17 changed files with 203 additions and 16 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue