Fast reload (#1445)

* Initial commit
* Fix crash on title
* Update level.cpp
* Update CHANGELOG.md
* Do slight audiotrack fade-ins and fade-outs on leveljumps
* Implement hash checks
* Fixes
* Rename rapid to fast
* Fixed flipmaps and bridge reinit
* Bypass reinitializing renderer for fast reload
* Fix issue when title and last loaded level are the same files
* Update CHANGELOG.md
* Additional fixes
* Wrap savegame loading code into a try-catch
* Update CHANGELOG.md
* Remove door collision on fast reload, restore stopper flag
* Display log message if level file was not found
* Update CHANGELOG.md
* Clear blocked flag for boxes
* Implement fast reload setting
* Add defaults to settings
* Consistent naming
* Smooth level loading audio crossfades
* Stop non-ambience soundtracks early
* Minor formatting
* Update Settings.lua 
---------

Co-authored-by: Jakub <80340234+Jakub768@users.noreply.github.com>
Co-authored-by: Sezz <sezzary@outlook.com>
This commit is contained in:
Lwmte 2024-11-11 13:55:50 +03:00 committed by GitHub
parent b79d662b13
commit 784f957596
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 605 additions and 358 deletions

View file

@ -6,7 +6,6 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
## Version 1.6 - xxxx-xx-xx
### Bug fixes
* Fixed engine performance around bridges.
* Fixed engine performance if weather or bubble effects are active.
* Fixed snow particles not always melting on the ground.
@ -22,7 +21,7 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
* Fixed Lens Flare object not functioning properly.
### Features/Amendments
* Added fast savegame reloading.
* Added ricochet sounds and make the effect more prominent.
### Lua API changes
@ -32,7 +31,6 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
## [Version 1.5](https://github.com/TombEngine/TombEditorReleases/releases/tag/v1.7.2) - 2024-11-03
### Bug fixes
* Fixed original issue with classic switch off trigger incorrectly activating some trigger actions.
* Fixed moveable status after antitriggering.
* Fixed leveljump vehicle transfer.
@ -71,7 +69,6 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
* Fixed young Lara hair drawing. https://tombengine.com/docs/level-settings/#young_lara
### Features/Amendments
* Added high framerate mode (also known as 60 FPS mode).
* Added a customisable global lensflare effect. https://tombengine.com/docs/level-settings/#lensflare
* Added a customisable starry sky and meteor effect. https://tombengine.com/docs/level-settings/#stars
@ -100,7 +97,6 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
* Removed original limit of 32 active Flame Emitters.
### Lua API changes
* Added Flow.EnableHomeLevel() function.
* Added Flow.IsStringPresent() function.
* Added Flow.LensFlare() and Flow.Starfield() classes.

View file

@ -114,6 +114,10 @@
<td class="name" ><a href="#errorMode">errorMode</a></td>
<td class="summary">How should the application respond to script errors?</td>
</tr>
<tr>
<td class="name" ><a href="#fastReload">fastReload</a></td>
<td class="summary">Can game utilize fast reload feature?</td>
</tr>
</table>
<br/>
@ -129,6 +133,7 @@
</dt>
<dd>
How should the application respond to script errors?
<br>
Must be one of the following:
<code>ErrorMode.TERMINATE</code> - print to the log file and return to the title level when any script error is hit.
This is the one you will want to go for if you want to know IMMEDIATELY if something has gone wrong.</p>
@ -151,6 +156,25 @@ has an unrecoverable error, the game will close.
</dd>
<dt>
<a name = "fastReload"></a>
<strong>fastReload</strong>
</dt>
<dd>
Can game utilize fast reload feature?
<br>
When set to <code>True</code>, game will attempt to perform fast savegame reloading, if current level is the same as
level loaded from the savegame. It will not work if level timestamp or checksum has changed (i.e. level was
updated). If set to <code>False</code> this functionality is turned off.
</dd>
</dl>

View file

@ -5,6 +5,7 @@ local Flow = TEN.Flow
local settings = Flow.Settings.new()
settings.errorMode = Flow.ErrorMode.WARN
settings.fastReload = true
Flow.SetSettings(settings)
local anims = Flow.Animations.new()

View file

@ -1,6 +1,7 @@
#pragma once
#include "Game/items.h"
#include "Math/Math.h"
#include "Specific/Clock.h"
struct CollisionInfo;
@ -67,7 +68,7 @@ enum CAMERA_FLAGS
CF_CHASE_OBJECT = 3,
};
constexpr auto FADE_SCREEN_SPEED = 16.0f / 255.0f;
constexpr auto FADE_SCREEN_SPEED = 2.0f / FPS;
constexpr auto DEFAULT_FOV = 80.0f;
extern CAMERA_INFO Camera;

View file

@ -105,8 +105,6 @@ int RequiredStartPos;
int CurrentLevel;
int NextLevel;
int SystemNameHash = 0;
bool InItemControlLoop;
short ItemNewRoomNo;
short ItemNewRooms[MAX_ROOMS];
@ -508,10 +506,8 @@ void InitializeOrLoadGame(bool loadGame)
g_Gui.SetEnterInventory(NO_VALUE);
// Restore game?
if (loadGame)
if (loadGame && SaveGame::Load(g_GameFlow->SelectedSaveGame))
{
SaveGame::Load(g_GameFlow->SelectedSaveGame);
InitializeGame = false;
g_GameFlow->SelectedSaveGame = 0;
@ -596,7 +592,7 @@ void EndGameLoop(int levelIndex, GameStatus reason)
DeInitializeScripting(levelIndex, reason);
StopAllSounds();
StopSoundTracks();
StopSoundTracks(SOUND_XFADETIME_LEVELJUMP, true);
StopRumble();
}

View file

@ -64,7 +64,6 @@ extern bool ThreadEnded;
extern int RequiredStartPos;
extern int CurrentLevel;
extern int NextLevel;
extern int SystemNameHash;
extern bool InItemControlLoop;
extern short ItemNewRoomNo;

View file

@ -7,14 +7,16 @@
#include "Game/control/lot.h"
#include "Game/control/volume.h"
#include "Game/items.h"
#include "Renderer/Renderer.h"
#include "Math/Math.h"
#include "Objects/game_object_ids.h"
#include "Objects/Generic/Doors/generic_doors.h"
#include "Renderer/Renderer.h"
#include "Specific/trutils.h"
using namespace TEN::Math;
using namespace TEN::Collision::Floordata;
using namespace TEN::Collision::Point;
using namespace TEN::Entities::Doors;
using namespace TEN::Renderer;
using namespace TEN::Utils;
@ -73,6 +75,74 @@ static void RemoveRoomFlipItems(const ROOM_INFO& room)
}
}
static void FlipRooms(int roomNumber, ROOM_INFO& activeRoom, ROOM_INFO& flippedRoom)
{
RemoveRoomFlipItems(activeRoom);
// Swap rooms.
std::swap(activeRoom, flippedRoom);
activeRoom.flippedRoom = flippedRoom.flippedRoom;
flippedRoom.flippedRoom = NO_VALUE;
activeRoom.itemNumber = flippedRoom.itemNumber;
activeRoom.fxNumber = flippedRoom.fxNumber;
AddRoomFlipItems(activeRoom);
// Update active room sectors.
for (auto& sector : activeRoom.Sectors)
sector.RoomNumber = roomNumber;
// Update flipped room sectors.
for (auto& sector : flippedRoom.Sectors)
sector.RoomNumber = activeRoom.flippedRoom;
// Update renderer data.
g_Renderer.FlipRooms(roomNumber, activeRoom.flippedRoom);
}
void ResetRoomData()
{
// Remove all door collisions.
for (const auto& item : g_Level.Items)
{
if (item.ObjectNumber == NO_VALUE || !item.Data.is<DOOR_DATA>())
continue;
auto& doorItem = g_Level.Items[item.Index];
auto& door = *(DOOR_DATA*)doorItem.Data;
if (door.opened)
continue;
OpenThatDoor(&door.d1, &door);
OpenThatDoor(&door.d2, &door);
OpenThatDoor(&door.d1flip, &door);
OpenThatDoor(&door.d2flip, &door);
door.opened = true;
}
// Unflip all rooms and remove all bridges and stopper flags.
for (int roomNumber = 0; roomNumber < g_Level.Rooms.size(); roomNumber++)
{
auto& room = g_Level.Rooms[roomNumber];
if (room.flippedRoom != NO_VALUE && room.flipNumber != NO_VALUE && FlipStats[room.flipNumber])
{
auto& flippedRoom = g_Level.Rooms[room.flippedRoom];
FlipRooms(roomNumber, room, flippedRoom);
}
for (auto& sector : room.Sectors)
{
sector.Stopper = false;
sector.BridgeItemNumbers.clear();
}
}
// Make sure no pathfinding boxes are blocked (either by doors or by other door-like objects).
for (int pathfindingBoxID = 0; pathfindingBoxID < g_Level.PathfindingBoxes.size(); pathfindingBoxID++)
g_Level.PathfindingBoxes[pathfindingBoxID].flags &= ~BLOCKED;
}
void DoFlipMap(int group)
{
if (group >= MAX_FLIPMAP)
@ -87,30 +157,10 @@ void DoFlipMap(int group)
auto& room = g_Level.Rooms[roomNumber];
// Handle flipmap.
if (room.flippedRoom >= 0 && room.flipNumber == group)
if (room.flippedRoom != NO_VALUE && room.flipNumber == group)
{
auto& flippedRoom = g_Level.Rooms[room.flippedRoom];
RemoveRoomFlipItems(room);
// Swap rooms.
std::swap(room, flippedRoom);
room.flippedRoom = flippedRoom.flippedRoom;
flippedRoom.flippedRoom = NO_VALUE;
room.itemNumber = flippedRoom.itemNumber;
room.fxNumber = flippedRoom.fxNumber;
AddRoomFlipItems(room);
g_Renderer.FlipRooms(roomNumber, room.flippedRoom);
// Update active room sectors.
for (auto& sector : room.Sectors)
sector.RoomNumber = roomNumber;
// Update flipped room sectors.
for (auto& sector : flippedRoom.Sectors)
sector.RoomNumber = room.flippedRoom;
FlipRooms(roomNumber, room, flippedRoom);
}
}

View file

@ -125,6 +125,7 @@ struct ROOM_INFO
};
void DoFlipMap(int group);
void ResetRoomData();
bool IsObjectInRoom(int roomNumber, GAME_OBJECT_ID objectID);
bool IsPointInRoom(const Vector3i& pos, int roomNumber);
int FindRoomNumber(const Vector3i& pos, int startRoomNumber = NO_VALUE, bool onlyNeighbors = false);

View file

@ -247,6 +247,7 @@ const std::vector<byte> SaveGame::Build()
Save::SaveGameHeaderBuilder sghb{ fbb };
sghb.add_level_name(levelNameOffset);
sghb.add_level_hash(LastLevelHash);
auto gameTime = GetGameTime(GameTimer);
sghb.add_days(gameTime.Days);
@ -1582,50 +1583,68 @@ bool SaveGame::Save(int slot)
bool SaveGame::Load(int slot)
{
if (!IsSaveGameSlotValid(slot))
{
TENLog("Savegame slot " + std::to_string(slot) + " is invalid, load is impossible.", LogLevel::Error);
return false;
}
if (!DoesSaveGameExist(slot))
{
TENLog("Savegame in slot " + std::to_string(slot) + " does not exist.", LogLevel::Error);
return false;
}
auto fileName = GetSavegameFilename(slot);
TENLog("Loading from savegame: " + fileName, LogLevel::Info);
std::ifstream file;
file.open(fileName, std::ios_base::app | std::ios_base::binary);
int size;
file.read(reinterpret_cast<char*>(&size), sizeof(size));
// Read current level save data.
std::vector<byte> saveData(size);
file.read(reinterpret_cast<char*>(saveData.data()), size);
// Reset hub data, as it's about to be replaced with saved one.
ResetHub();
// Read hub data from savegame.
int hubCount;
file.read(reinterpret_cast<char*>(&hubCount), sizeof(hubCount));
TENLog("Hub count: " + std::to_string(hubCount), LogLevel::Info);
for (int i = 0; i < hubCount; i++)
auto file = std::ifstream();
try
{
int index;
file.read(reinterpret_cast<char*>(&index), sizeof(index));
file.open(fileName, std::ios_base::app | std::ios_base::binary);
int size = 0;
file.read(reinterpret_cast<char*>(&size), sizeof(size));
std::vector<byte> hubBuffer(size);
file.read(reinterpret_cast<char*>(hubBuffer.data()), size);
Hub[index] = hubBuffer;
// Read current level save data.
auto saveData = std::vector<byte>(size);
file.read(reinterpret_cast<char*>(saveData.data()), size);
// Reset hub data, as it's about to be replaced with saved one.
ResetHub();
// Read hub data from savegame.
int hubCount = 0;
file.read(reinterpret_cast<char*>(&hubCount), sizeof(hubCount));
TENLog("Hub count: " + std::to_string(hubCount), LogLevel::Info);
for (int i = 0; i < hubCount; i++)
{
int index = 0;
file.read(reinterpret_cast<char*>(&index), sizeof(index));
file.read(reinterpret_cast<char*>(&size), sizeof(size));
auto hubBuffer = std::vector<byte>(size);
file.read(reinterpret_cast<char*>(hubBuffer.data()), size);
Hub[index] = hubBuffer;
}
file.close();
// Load save data for current level.
Parse(saveData, false);
return true;
}
catch (std::exception& ex)
{
TENLog("Error while loading savegame: " + std::string(ex.what()), LogLevel::Error);
if (file.is_open())
file.close();
}
file.close();
// Load save data for current level.
Parse(saveData, false);
return true;
return false;
}
static void ParseStatistics(const Save::SaveGame* s, bool isHub)
@ -2049,7 +2068,7 @@ static void ParseEffects(const Save::SaveGame* s)
TENAssert(i < (int)SoundTrackType::Count, "Soundtrack type count was changed");
auto track = s->soundtracks()->Get(i);
PlaySoundTrack(track->name()->str(), (SoundTrackType)i, track->position());
PlaySoundTrack(track->name()->str(), (SoundTrackType)i, track->position(), SOUND_XFADETIME_LEVELJUMP);
}
// Load fish swarm.
@ -2655,6 +2674,7 @@ bool SaveGame::LoadHeader(int slot, SaveGameHeader* header)
header->Level = s->header()->level();
header->LevelName = s->header()->level_name()->str();
header->LevelHash = s->header()->level_hash();
header->Days = s->header()->days();
header->Hours = s->header()->hours();
header->Minutes = s->header()->minutes();

View file

@ -11,32 +11,33 @@ constexpr auto SAVEGAME_MAX = 16;
struct Stats
{
unsigned int Timer;
unsigned int Distance;
unsigned int AmmoHits;
unsigned int AmmoUsed;
unsigned int HealthUsed;
unsigned int Kills;
unsigned int Secrets;
unsigned int Timer = 0;
unsigned int Distance = 0;
unsigned int AmmoHits = 0;
unsigned int AmmoUsed = 0;
unsigned int HealthUsed = 0;
unsigned int Kills = 0;
unsigned int Secrets = 0;
};
struct GameStats
{
Stats Game;
Stats Level;
Stats Game = {};
Stats Level = {};
};
struct SaveGameHeader
{
std::string LevelName;
int Days;
int Hours;
int Minutes;
int Seconds;
int Level;
int Timer;
int Count;
bool Present;
std::string LevelName = {};
int LevelHash = 0;
int Days = 0;
int Hours = 0;
int Minutes = 0;
int Seconds = 0;
int Level = 0;
int Timer = 0;
int Count = 0;
bool Present = false;
};
class SaveGame

View file

@ -92,7 +92,7 @@ namespace TEN::Entities::Doors
doorData->d1.block = (boxNumber != NO_VALUE && g_Level.PathfindingBoxes[boxNumber].flags & BLOCKABLE) ? boxNumber : NO_VALUE;
doorData->d1.data = *doorData->d1.floor;
if (r->flippedRoom != -1)
if (r->flippedRoom != NO_VALUE)
{
r = &g_Level.Rooms[r->flippedRoom];
doorData->d1flip.floor = GetSector(r, doorItem->Pose.Position.x - r->Position.x + xOffset, doorItem->Pose.Position.z - r->Position.z + zOffset);

View file

@ -22,6 +22,8 @@ namespace TEN::Renderer
bool Renderer::PrepareDataForTheRenderer()
{
TENLog("Preparing renderer...", LogLevel::Info);
_lastBlendMode = BlendMode::Unknown;
_lastCullMode = CullMode::Unknown;
_lastDepthState = DepthState::Unknown;

View file

@ -1013,10 +1013,6 @@ namespace TEN::Renderer
_context->VSSetShader(_vsInventory.Get(), nullptr, 0);
_context->PSSetShader(_psInventory.Get(), nullptr, 0);
// Set texture
BindTexture(TextureRegister::ColorMap, &std::get<0>(_moveablesTextures[0]), SamplerStateRegister::AnisotropicClamp);
BindTexture(TextureRegister::NormalMap, &std::get<1>(_moveablesTextures[0]), SamplerStateRegister::AnisotropicClamp);
if (CurrentLevel == 0)
{
auto titleMenu = g_Gui.GetMenuToDisplay();
@ -1047,6 +1043,14 @@ namespace TEN::Renderer
}
else
{
if (g_Gui.GetInventoryMode() == InventoryMode::InGame ||
g_Gui.GetInventoryMode() == InventoryMode::Examine)
{
// Set texture.
BindTexture(TextureRegister::ColorMap, &std::get<0>(_moveablesTextures[0]), SamplerStateRegister::AnisotropicClamp);
BindTexture(TextureRegister::NormalMap, &std::get<1>(_moveablesTextures[0]), SamplerStateRegister::AnisotropicClamp);
}
switch (g_Gui.GetInventoryMode())
{
case InventoryMode::Load:
@ -1103,26 +1107,30 @@ namespace TEN::Renderer
void Renderer::RenderLoadingScreen(float percentage)
{
// Set basic render states
// Set basic render states.
SetBlendMode(BlendMode::Opaque);
SetCullMode(CullMode::CounterClockwise);
do
{
// Clear screen
// Clear screen.
_context->ClearRenderTargetView(_backBuffer.RenderTargetView.Get(), Colors::Black);
_context->ClearDepthStencilView(_backBuffer.DepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
// Bind the back buffer
// Bind back buffer.
_context->OMSetRenderTargets(1, _backBuffer.RenderTargetView.GetAddressOf(), _backBuffer.DepthStencilView.Get());
_context->RSSetViewports(1, &_viewport);
ResetScissor();
// Draw the full screen background
// Draw fullscreen background. If unavailable, draw last dumped game scene.
if (_loadingScreenTexture.Texture)
DrawFullScreenQuad(
_loadingScreenTexture.ShaderResourceView.Get(),
Vector3(ScreenFadeCurrent, ScreenFadeCurrent, ScreenFadeCurrent));
{
DrawFullScreenQuad(_loadingScreenTexture.ShaderResourceView.Get(), Vector3(ScreenFadeCurrent, ScreenFadeCurrent, ScreenFadeCurrent));
}
else if (_dumpScreenRenderTarget.Texture)
{
DrawFullScreenQuad(_dumpScreenRenderTarget.ShaderResourceView.Get(), Vector3(ScreenFadeCurrent, ScreenFadeCurrent, ScreenFadeCurrent));
}
if (ScreenFadeCurrent && percentage > 0.0f && percentage < 100.0f)
DrawLoadingBar(percentage);

View file

@ -64,8 +64,10 @@ namespace TEN::Renderer
texture = Texture2D();
if (std::filesystem::is_regular_file(path))
{
texture = Texture2D(_device.Get(), path);
else
}
else if (!path.empty()) // Loading default texture without path may be intentional.
{
std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
TENLog("Texture file not found: " + converter.to_bytes(path), LogLevel::Warning);

View file

@ -1,5 +1,7 @@
#pragma once
#include "Game/control/control.h"
#include "Scripting/Internal/TEN/Flow/Settings/Settings.h"
class ScriptInterfaceLevel;
@ -25,6 +27,7 @@ public:
virtual ~ScriptInterfaceFlowHandler() = default;
virtual void LoadFlowScript() = 0;
virtual Settings* GetSettings() = 0;
virtual void SetGameDir(const std::string& assetDir) = 0;
virtual std::string GetGameDir() = 0;

View file

@ -238,6 +238,7 @@ static constexpr char ScriptReserved_LaraType[] = "LaraType";
static constexpr char ScriptReserved_RotationAxis[] = "RotationAxis";
static constexpr char ScriptReserved_ItemAction[] = "ItemAction";
static constexpr char ScriptReserved_ErrorMode[] = "ErrorMode";
static constexpr char ScriptReserved_FastReload[] = "FastReload";
static constexpr char ScriptReserved_InventoryItem[] = "InventoryItem";
static constexpr char ScriptReserved_LaraWeaponType[] = "LaraWeaponType";
static constexpr char ScriptReserved_PlayerAmmoType[] = "PlayerAmmoType";

View file

@ -1,19 +1,19 @@
#include "framework.h"
#include "Settings.h"
#include "Scripting/Internal/TEN/Flow/Settings/Settings.h"
/***
Settings that will be run on game startup.
@tenclass Flow.Settings
@pragma nostrip
*/
/// Settings that will be run on game startup.
// @tenclass Flow.Settings
// @pragma nostrip
void Settings::Register(sol::table & parent)
void Settings::Register(sol::table& parent)
{
parent.new_usertype<Settings>("Settings",
parent.new_usertype<Settings>(
"Settings",
sol::constructors<Settings()>(),
sol::call_constructor, sol::constructors<Settings>(),
/*** How should the application respond to script errors?
<br>
Must be one of the following:
`ErrorMode.TERMINATE` - print to the log file and return to the title level when any script error is hit.
This is the one you will want to go for if you want to know IMMEDIATELY if something has gone wrong.
@ -31,6 +31,14 @@ has an unrecoverable error, the game will close.
@mem errorMode
*/
"errorMode", &Settings::ErrorMode
);
"errorMode", &Settings::ErrorMode,
/// Can the game utilize the fast reload feature?
// <br>
// When set to `true`, the game will attempt to perform fast savegame reloading if current level is the same as
// the level loaded from the savegame. It will not work if the level timestamp or checksum has changed
// (i.e. level was updated). If set to `false`, this functionality is turned off.
//
// @mem fastReload
"fastReload", &Settings::FastReload);
}

View file

@ -1,22 +1,20 @@
#pragma once
#include "Scripting/Internal/ScriptAssert.h"
#include <string>
static const std::unordered_map<std::string, ErrorMode> ERROR_MODES {
{"SILENT", ErrorMode::Silent},
{"WARN", ErrorMode::Warn},
{"TERMINATE", ErrorMode::Terminate}
namespace sol { class state; }
static const std::unordered_map<std::string, ErrorMode> ERROR_MODES
{
{ "SILENT", ErrorMode::Silent },
{ "WARN", ErrorMode::Warn },
{ "TERMINATE", ErrorMode::Terminate }
};
namespace sol {
class state;
}
struct Settings
{
ErrorMode ErrorMode;
ErrorMode ErrorMode = ErrorMode::Warn;
bool FastReload = true;
static void Register(sol::table & parent);
static void Register(sol::table& parent);
};

View file

@ -490,7 +490,7 @@ std::optional<std::string> GetCurrentSubtitle()
return std::nullopt;
}
void PlaySoundTrack(const std::string& track, SoundTrackType mode, QWORD position)
void PlaySoundTrack(const std::string& track, SoundTrackType mode, QWORD position, int forceFadeInTime)
{
if (!g_Configuration.EnableSound)
return;
@ -565,11 +565,11 @@ void PlaySoundTrack(const std::string& track, SoundTrackType mode, QWORD positio
// BGM tracks are crossfaded, and additionally shuffled a bit to make things more natural.
// Think everybody are fed up with same start-up sounds of Caves ambience...
if (crossfade && BASS_ChannelIsActive(SoundtrackSlot[(int)SoundTrackType::BGM].Channel))
if (forceFadeInTime > 0 || (crossfade && BASS_ChannelIsActive(SoundtrackSlot[(int)SoundTrackType::BGM].Channel)))
{
// Crossfade...
BASS_ChannelSetAttribute(stream, BASS_ATTRIB_VOL, 0.0f);
BASS_ChannelSlideAttribute(stream, BASS_ATTRIB_VOL, masterVolume, crossfadeTime);
BASS_ChannelSlideAttribute(stream, BASS_ATTRIB_VOL, masterVolume, (forceFadeInTime > 0) ? forceFadeInTime : crossfadeTime);
// Shuffle...
// Only activates if no custom position is passed as argument.
@ -659,7 +659,7 @@ void PlaySoundTrack(int index, short mask)
PlaySoundTrack(SoundTracks[index].Name, SoundTracks[index].Mode);
}
void StopSoundTracks(bool excludeAmbience)
void StopSoundTracks(int fadeoutTime, bool excludeAmbience)
{
for (int i = 0; i < (int)SoundTrackType::Count; i++)
{
@ -667,12 +667,15 @@ void StopSoundTracks(bool excludeAmbience)
if (excludeAmbience && type == SoundTrackType::BGM)
continue;
StopSoundTrack(type, SOUND_XFADETIME_ONESHOT);
StopSoundTrack(type, fadeoutTime);
}
}
void StopSoundTrack(SoundTrackType mode, int fadeoutTime)
{
if (SoundtrackSlot[(int)mode].Channel == NULL)
return;
// Do fadeout.
BASS_ChannelSlideAttribute(SoundtrackSlot[(int)mode].Channel, BASS_ATTRIB_VOL | BASS_SLIDE_LOG, -1.0f, fadeoutTime);

View file

@ -25,6 +25,7 @@ constexpr auto SOUND_MILLISECONDS_IN_SECOND = 1000.0f;
constexpr auto SOUND_XFADETIME_BGM = 5000;
constexpr auto SOUND_XFADETIME_BGM_START = 1500;
constexpr auto SOUND_XFADETIME_ONESHOT = 200;
constexpr auto SOUND_XFADETIME_LEVELJUMP = 400;
constexpr auto SOUND_XFADETIME_CUTSOUND = 100;
constexpr auto SOUND_XFADETIME_HIJACKSOUND = 50;
constexpr auto SOUND_BGM_DAMP_COEFFICIENT = 0.5f;
@ -164,11 +165,11 @@ void SayNo();
void PlaySoundSources();
int GetShatterSound(int shatterID);
void PlaySoundTrack(const std::string& trackName, SoundTrackType mode, QWORD position = 0);
void PlaySoundTrack(const std::string& trackName, SoundTrackType mode, QWORD position = 0, int forceFadeInTime = 0);
void PlaySoundTrack(const std::string& trackName, short mask = 0);
void PlaySoundTrack(int index, short mask = 0);
void StopSoundTrack(SoundTrackType mode, int fadeoutTime);
void StopSoundTracks(bool excludeAmbience = false);
void StopSoundTracks(int fadeoutTime = SOUND_XFADETIME_LEVELJUMP, bool excludeAmbience = false);
void ClearSoundTrackMasks();
void PlaySecretTrack();
void EnumerateLegacyTracks();

View file

@ -77,44 +77,54 @@ const std::vector<GAME_OBJECT_ID> BRIDGE_OBJECT_IDS =
ID_BRIDGE_CUSTOM
};
char* LevelDataPtr;
LEVEL g_Level;
std::vector<int> MoveablesIds;
std::vector<int> StaticObjectsIds;
std::vector<int> SpriteSequencesIds;
LEVEL g_Level;
char* DataPtr;
char* CurrentDataPtr;
bool FirstLevel = true;
int SystemNameHash = 0;
int LastLevelHash = 0;
std::filesystem::file_time_type LastLevelTimestamp;
std::string LastLevelFilePath;
unsigned char ReadUInt8()
{
unsigned char value = *(unsigned char*)LevelDataPtr;
LevelDataPtr += 1;
unsigned char value = *(unsigned char*)CurrentDataPtr;
CurrentDataPtr += 1;
return value;
}
short ReadInt16()
{
short value = *(short*)LevelDataPtr;
LevelDataPtr += 2;
short value = *(short*)CurrentDataPtr;
CurrentDataPtr += 2;
return value;
}
unsigned short ReadUInt16()
{
unsigned short value = *(unsigned short*)LevelDataPtr;
LevelDataPtr += 2;
unsigned short value = *(unsigned short*)CurrentDataPtr;
CurrentDataPtr += 2;
return value;
}
int ReadInt32()
{
int value = *(int*)LevelDataPtr;
LevelDataPtr += 4;
int value = *(int*)CurrentDataPtr;
CurrentDataPtr += 4;
return value;
}
float ReadFloat()
{
float value = *(float*)LevelDataPtr;
LevelDataPtr += 4;
float value = *(float*)CurrentDataPtr;
CurrentDataPtr += 4;
return value;
}
@ -152,8 +162,8 @@ bool ReadBool()
void ReadBytes(void* dest, int count)
{
memcpy(dest, LevelDataPtr, count);
LevelDataPtr += count;
memcpy(dest, CurrentDataPtr, count);
CurrentDataPtr += count;
}
long long ReadLEB128(bool sign)
@ -188,9 +198,9 @@ std::string ReadString()
return std::string();
else
{
auto newPtr = LevelDataPtr + numBytes;
auto result = std::string(LevelDataPtr, newPtr);
LevelDataPtr = newPtr;
auto newPtr = CurrentDataPtr + numBytes;
auto result = std::string(CurrentDataPtr, newPtr);
CurrentDataPtr = newPtr;
return result;
}
}
@ -205,54 +215,51 @@ void LoadItems()
InitializeItemArray(ITEM_COUNT_MAX);
if (g_Level.NumItems > 0)
for (int i = 0; i < g_Level.NumItems; i++)
{
for (int i = 0; i < g_Level.NumItems; i++)
{
auto* item = &g_Level.Items[i];
auto* item = &g_Level.Items[i];
item->Data = ItemData{};
item->ObjectNumber = from_underlying(ReadInt16());
item->RoomNumber = ReadInt16();
item->Pose.Position.x = ReadInt32();
item->Pose.Position.y = ReadInt32();
item->Pose.Position.z = ReadInt32();
item->Pose.Orientation.y = ReadInt16();
item->Pose.Orientation.x = ReadInt16();
item->Pose.Orientation.z = ReadInt16();
item->Model.Color = ReadVector4();
item->TriggerFlags = ReadInt16();
item->Flags = ReadInt16();
item->Name = ReadString();
item->Data = ItemData{};
item->ObjectNumber = from_underlying(ReadInt16());
item->RoomNumber = ReadInt16();
item->Pose.Position.x = ReadInt32();
item->Pose.Position.y = ReadInt32();
item->Pose.Position.z = ReadInt32();
item->Pose.Orientation.y = ReadInt16();
item->Pose.Orientation.x = ReadInt16();
item->Pose.Orientation.z = ReadInt16();
item->Model.Color = ReadVector4();
item->TriggerFlags = ReadInt16();
item->Flags = ReadInt16();
item->Name = ReadString();
g_GameScriptEntities->AddName(item->Name, (short)i);
g_GameScriptEntities->TryAddColliding((short)i);
g_GameScriptEntities->AddName(item->Name, (short)i);
g_GameScriptEntities->TryAddColliding((short)i);
memcpy(&item->StartPose, &item->Pose, sizeof(Pose));
}
memcpy(&item->StartPose, &item->Pose, sizeof(Pose));
}
// Initialize items.
for (int i = 0; i <= 1; i++)
// Initialize items.
for (int i = 0; i <= 1; i++)
{
// HACK: Initialize bridges first. Required because other items need final floordata to init properly.
if (i == 0)
{
// HACK: Initialize bridges first. Required because other items need final floordata to init properly.
if (i == 0)
for (int j = 0; j < g_Level.NumItems; j++)
{
for (int j = 0; j < g_Level.NumItems; j++)
{
const auto& item = g_Level.Items[j];
if (Contains(BRIDGE_OBJECT_IDS, item.ObjectNumber))
InitializeItem(j);
}
const auto& item = g_Level.Items[j];
if (Contains(BRIDGE_OBJECT_IDS, item.ObjectNumber))
InitializeItem(j);
}
// Initialize non-bridge items second.
else if (i == 1)
}
// Initialize non-bridge items second.
else if (i == 1)
{
for (int j = 0; j < g_Level.NumItems; j++)
{
for (int j = 0; j < g_Level.NumItems; j++)
{
const auto& item = g_Level.Items[j];
if (!item.IsBridge())
InitializeItem(j);
}
const auto& item = g_Level.Items[j];
if (!item.IsBridge())
InitializeItem(j);
}
}
}
@ -674,7 +681,95 @@ static Plane ConvertFakePlaneToPlane(const Vector3& fakePlane, bool isFloor)
return Plane(normal, dist);
}
void ReadRooms()
void LoadDynamicRoomData()
{
int roomCount = ReadInt32();
if (g_Level.Rooms.size() != roomCount)
throw std::exception("Dynamic room data count is inconsistent with room count");
for (int i = 0; i < roomCount; i++)
{
auto& room = g_Level.Rooms[i];
room.Name = ReadString();
int tagCount = ReadInt32();
room.Tags.resize(0);
room.Tags.reserve(tagCount);
for (int j = 0; j < tagCount; j++)
room.Tags.push_back(ReadString());
room.ambient = ReadVector3();
room.flippedRoom = ReadInt32();
room.flags = ReadInt32();
room.meshEffect = ReadInt32();
room.reverbType = (ReverbType)ReadInt32();
room.flipNumber = ReadInt32();
int staticCount = ReadInt32();
room.mesh.resize(0);
room.mesh.reserve(staticCount);
for (int j = 0; j < staticCount; j++)
{
auto& mesh = room.mesh.emplace_back();
mesh.roomNumber = i;
mesh.pos.Position.x = ReadInt32();
mesh.pos.Position.y = ReadInt32();
mesh.pos.Position.z = ReadInt32();
mesh.pos.Orientation.y = ReadUInt16();
mesh.pos.Orientation.x = ReadUInt16();
mesh.pos.Orientation.z = ReadUInt16();
mesh.scale = ReadFloat();
mesh.flags = ReadUInt16();
mesh.color = ReadVector4();
mesh.staticNumber = ReadUInt16();
mesh.HitPoints = ReadInt16();
mesh.Name = ReadString();
g_GameScriptEntities->AddName(mesh.Name, mesh);
}
int triggerVolumeCount = ReadInt32();
room.TriggerVolumes.resize(0);
room.TriggerVolumes.reserve(triggerVolumeCount);
for (int j = 0; j < triggerVolumeCount; j++)
{
auto& volume = room.TriggerVolumes.emplace_back();
volume.Type = (VolumeType)ReadInt32();
auto pos = ReadVector3();
auto orient = ReadVector4();
auto scale = ReadVector3();
volume.Enabled = ReadBool();
volume.DetectInAdjacentRooms = ReadBool();
volume.Name = ReadString();
volume.EventSetIndex = ReadInt32();
volume.Box = BoundingOrientedBox(pos, scale, orient);
volume.Sphere = BoundingSphere(pos, scale.x);
volume.StateQueue.reserve(VOLUME_STATE_QUEUE_SIZE);
g_GameScriptEntities->AddName(volume.Name, volume);
}
g_GameScriptEntities->AddName(room.Name, room);
room.itemNumber = NO_VALUE;
room.fxNumber = NO_VALUE;
}
}
void LoadStaticRoomData()
{
constexpr auto ILLEGAL_FLOOR_SLOPE_ANGLE = ANGLE(36.0f);
constexpr auto ILLEGAL_CEILING_SLOPE_ANGLE = ANGLE(45.0f);
@ -687,12 +782,6 @@ void ReadRooms()
{
auto& room = g_Level.Rooms.emplace_back();
room.Name = ReadString();
int tagCount = ReadInt32();
for (int j = 0; j < tagCount; j++)
room.Tags.push_back(ReadString());
room.Position.x = ReadInt32();
room.Position.y = 0;
room.Position.z = ReadInt32();
@ -824,8 +913,6 @@ void ReadRooms()
}
}
room.ambient = ReadVector3();
int lightCount = ReadInt32();
room.lights.reserve(lightCount);
for (int j = 0; j < lightCount; j++)
@ -851,67 +938,8 @@ void ReadRooms()
room.lights.push_back(light);
}
int staticCount = ReadInt32();
room.mesh.reserve(staticCount);
for (int j = 0; j < staticCount; j++)
{
auto& mesh = room.mesh.emplace_back();
mesh.roomNumber = i;
mesh.pos.Position.x = ReadInt32();
mesh.pos.Position.y = ReadInt32();
mesh.pos.Position.z = ReadInt32();
mesh.pos.Orientation.y = ReadUInt16();
mesh.pos.Orientation.x = ReadUInt16();
mesh.pos.Orientation.z = ReadUInt16();
mesh.scale = ReadFloat();
mesh.flags = ReadUInt16();
mesh.color = ReadVector4();
mesh.staticNumber = ReadUInt16();
mesh.HitPoints = ReadInt16();
mesh.Name = ReadString();
g_GameScriptEntities->AddName(mesh.Name, mesh);
}
int triggerVolumeCount = ReadInt32();
room.TriggerVolumes.reserve(triggerVolumeCount);
for (int j = 0; j < triggerVolumeCount; j++)
{
auto& volume = room.TriggerVolumes.emplace_back();
volume.Type = (VolumeType)ReadInt32();
auto pos = ReadVector3();
auto orient = ReadVector4();
auto scale = ReadVector3();
volume.Enabled = ReadBool();
volume.DetectInAdjacentRooms = ReadBool();
volume.Name = ReadString();
volume.EventSetIndex = ReadInt32();
volume.Box = BoundingOrientedBox(pos, scale, orient);
volume.Sphere = BoundingSphere(pos, scale.x);
volume.StateQueue.reserve(VOLUME_STATE_QUEUE_SIZE);
g_GameScriptEntities->AddName(volume.Name, volume);
}
room.flippedRoom = ReadInt32();
room.flags = ReadInt32();
room.meshEffect = ReadInt32();
room.reverbType = (ReverbType)ReadInt32();
room.flipNumber = ReadInt32();
room.itemNumber = NO_VALUE;
room.fxNumber = NO_VALUE;
room.RoomNumber = i;
g_GameScriptEntities->AddName(room.Name, room);
}
}
@ -921,7 +949,7 @@ void LoadRooms()
Wibble = 0;
ReadRooms();
LoadStaticRoomData();
BuildOutsideRoomsTable();
int numFloorData = ReadInt32();
@ -929,15 +957,39 @@ void LoadRooms()
ReadBytes(g_Level.FloorData.data(), numFloorData * sizeof(short));
}
void FreeLevel()
void FreeLevel(bool partial)
{
static bool firstLevel = true;
if (firstLevel)
if (FirstLevel)
{
firstLevel = false;
FirstLevel = false;
return;
}
// Should happen before resetting items.
if (partial)
ResetRoomData();
g_Level.Items.resize(0);
g_Level.AIObjects.resize(0);
g_Level.Cameras.resize(0);
g_Level.Sinks.resize(0);
g_Level.SoundSources.resize(0);
g_Level.VolumeEventSets.resize(0);
g_Level.GlobalEventSets.resize(0);
g_Level.LoopedEventSetIndices.resize(0);
g_GameScript->FreeLevelScripts();
g_GameScriptEntities->FreeEntities();
if (partial)
return;
g_Renderer.FreeRendererData();
MoveablesIds.resize(0);
StaticObjectsIds.resize(0);
SpriteSequencesIds.resize(0);
g_Level.RoomTextures.resize(0);
g_Level.MoveablesTextures.resize(0);
g_Level.StaticsTextures.resize(0);
@ -947,8 +999,6 @@ void FreeLevel()
g_Level.Rooms.resize(0);
g_Level.Bones.resize(0);
g_Level.Meshes.resize(0);
MoveablesIds.resize(0);
SpriteSequencesIds.resize(0);
g_Level.PathfindingBoxes.resize(0);
g_Level.Overlaps.resize(0);
g_Level.Anims.resize(0);
@ -960,14 +1010,6 @@ void FreeLevel()
g_Level.SoundDetails.resize(0);
g_Level.SoundMap.resize(0);
g_Level.FloorData.resize(0);
g_Level.Cameras.resize(0);
g_Level.Sinks.resize(0);
g_Level.SoundSources.resize(0);
g_Level.AIObjects.resize(0);
g_Level.VolumeEventSets.resize(0);
g_Level.GlobalEventSets.resize(0);
g_Level.LoopedEventSetIndices.resize(0);
g_Level.Items.resize(0);
for (int i = 0; i < 2; i++)
{
@ -975,10 +1017,6 @@ void FreeLevel()
g_Level.Zones[j][i].clear();
}
g_Renderer.FreeRendererData();
g_GameScript->FreeLevelScripts();
g_GameScriptEntities->FreeEntities();
FreeSamples();
}
@ -1161,46 +1199,88 @@ bool Decompress(byte* dest, byte* src, unsigned long compressedSize, unsigned lo
return false;
}
bool LoadLevel(int levelIndex)
bool ReadCompressedBlock(FILE* filePtr, bool skip)
{
auto* level = g_GameFlow->GetLevel(levelIndex);
int compressedSize = 0;
int uncompressedSize = 0;
auto assetDir = g_GameFlow->GetGameDir();
auto levelPath = assetDir + level->FileName;
TENLog("Loading level file: " + levelPath, LogLevel::Info);
ReadFileEx(&uncompressedSize, 1, 4, filePtr);
ReadFileEx(&compressedSize, 1, 4, filePtr);
LevelDataPtr = nullptr;
if (skip)
{
fseek(filePtr, compressedSize, SEEK_CUR);
return false;
}
auto compressedBuffer = (char*)malloc(compressedSize);
ReadFileEx(compressedBuffer, compressedSize, 1, filePtr);
DataPtr = (char*)malloc(uncompressedSize);
Decompress((byte*)DataPtr, (byte*)compressedBuffer, compressedSize, uncompressedSize);
free(compressedBuffer);
CurrentDataPtr = DataPtr;
return true;
}
void FinalizeBlock()
{
if (DataPtr == nullptr)
return;
free(DataPtr);
DataPtr = nullptr;
CurrentDataPtr = nullptr;
}
void UpdateProgress(float progress, bool skip = false)
{
if (skip)
return;
g_Renderer.UpdateProgress(progress);
}
bool LoadLevel(std::string path, bool partial)
{
FILE* filePtr = nullptr;
char* dataPtr = nullptr;
bool LoadedSuccessfully;
auto loadingScreenPath = TEN::Utils::ToWString(assetDir + level->LoadScreenFileName);
g_Renderer.SetLoadingScreen(loadingScreenPath);
SetScreenFadeIn(FADE_SCREEN_SPEED, true);
g_Renderer.UpdateProgress(0);
bool loadedSuccessfully = false;
try
{
filePtr = FileOpen(levelPath.c_str());
filePtr = FileOpen(path.c_str());
if (!filePtr)
throw std::exception{ (std::string{ "Unable to read level file: " } + levelPath).c_str() };
throw std::exception{ (std::string{ "Unable to read level file: " } + path).c_str() };
char header[4];
unsigned char version[4];
int compressedSize;
int uncompressedSize;
int systemHash;
int systemHash = 0;
int levelHash = 0;
// Read file header
ReadFileEx(&header, 1, 4, filePtr);
ReadFileEx(&version, 1, 4, filePtr);
ReadFileEx(&systemHash, 1, 4, filePtr);
ReadFileEx(&levelHash, 1, 4, filePtr);
// Check file header
// Check file header.
if (std::string(header) != "TEN")
throw std::invalid_argument("Level file header is not valid! Must be TEN. Probably old level version?");
// Check level file integrity to allow or disallow fast reload.
if (partial && levelHash != LastLevelHash)
{
TENLog("Level file has changed since the last load; fast reload is not possible.", LogLevel::Warning);
partial = false;
FreeLevel(false); // Erase all precached data.
}
// Store information about last loaded level file.
LastLevelFilePath = path;
LastLevelHash = levelHash;
LastLevelTimestamp = std::filesystem::last_write_time(path);
TENLog("Level compiler version: " + std::to_string(version[0]) + "." + std::to_string(version[1]) + "." + std::to_string(version[2]), LogLevel::Info);
@ -1225,95 +1305,105 @@ bool LoadLevel(int levelIndex)
SystemNameHash = 0;
}
// Read data sizes
ReadFileEx(&uncompressedSize, 1, 4, filePtr);
ReadFileEx(&compressedSize, 1, 4, filePtr);
if (partial)
{
TENLog("Loading same level. Skipping media and geometry data.", LogLevel::Info);
SetScreenFadeOut(FADE_SCREEN_SPEED * 2, true);
}
else
{
SetScreenFadeIn(FADE_SCREEN_SPEED, true);
}
// The entire level is ZLIB compressed
auto compressedBuffer = (char*)malloc(compressedSize);
dataPtr = (char*)malloc(uncompressedSize);
LevelDataPtr = dataPtr;
UpdateProgress(0);
ReadFileEx(compressedBuffer, compressedSize, 1, filePtr);
Decompress((byte*)LevelDataPtr, (byte*)compressedBuffer, compressedSize, uncompressedSize);
// Media block
if (ReadCompressedBlock(filePtr, partial))
{
LoadTextures();
UpdateProgress(30);
// Now the entire level is decompressed, we can close it
free(compressedBuffer);
FileClose(filePtr);
filePtr = nullptr;
LoadSamples();
UpdateProgress(40);
LoadTextures();
FinalizeBlock();
}
g_Renderer.UpdateProgress(20);
// Geometry block
if (ReadCompressedBlock(filePtr, partial))
{
LoadRooms();
UpdateProgress(50);
LoadRooms();
g_Renderer.UpdateProgress(40);
LoadObjects();
UpdateProgress(60);
LoadObjects();
g_Renderer.UpdateProgress(50);
LoadSprites();
LoadBoxes();
LoadAnimatedTextures();
UpdateProgress(70);
LoadSprites();
LoadCameras();
LoadSoundSources();
g_Renderer.UpdateProgress(60);
FinalizeBlock();
}
LoadBoxes();
// Dynamic data block
if (ReadCompressedBlock(filePtr, false))
{
LoadDynamicRoomData();
LoadItems();
LoadAIObjects();
LoadCameras();
LoadSoundSources();
LoadEventSets();
UpdateProgress(80, partial);
//InitializeLOTarray(true);
LoadAnimatedTextures();
g_Renderer.UpdateProgress(70);
LoadItems();
LoadAIObjects();
LoadEventSets();
LoadSamples();
g_Renderer.UpdateProgress(80);
FinalizeBlock();
}
TENLog("Initializing level...", LogLevel::Info);
// Initialize the game
// Initialize game.
InitializeGameFlags();
InitializeLara(!InitializeGame && CurrentLevel > 0);
InitializeNeighborRoomList();
GetCarriedItems();
GetAIPickups();
g_GameScriptEntities->AssignLara();
g_Renderer.UpdateProgress(90);
UpdateProgress(90, partial);
TENLog("Preparing renderer...", LogLevel::Info);
if (!partial)
{
g_Renderer.PrepareDataForTheRenderer();
SetScreenFadeOut(FADE_SCREEN_SPEED, true);
StopSoundTracks(SOUND_XFADETIME_BGM_START);
}
else
{
SetScreenFadeIn(FADE_SCREEN_SPEED, true);
StopSoundTracks(SOUND_XFADETIME_LEVELJUMP);
}
g_Renderer.PrepareDataForTheRenderer();
UpdateProgress(100, partial);
TENLog("Level loading complete.", LogLevel::Info);
SetScreenFadeOut(FADE_SCREEN_SPEED, true);
g_Renderer.UpdateProgress(100);
LoadedSuccessfully = true;
loadedSuccessfully = true;
}
catch (std::exception& ex)
{
if (filePtr)
{
FileClose(filePtr);
filePtr = nullptr;
}
FinalizeBlock();
StopSoundTracks(SOUND_XFADETIME_LEVELJUMP);
TENLog("Error while loading level: " + std::string(ex.what()), LogLevel::Error);
LoadedSuccessfully = false;
loadedSuccessfully = false;
SystemNameHash = 0;
}
if (dataPtr)
{
free(dataPtr);
dataPtr = LevelDataPtr = nullptr;
}
// Now the entire level is decompressed, we can close it
FileClose(filePtr);
filePtr = nullptr;
return LoadedSuccessfully;
return loadedSuccessfully;
}
void LoadSamples()
@ -1386,7 +1476,7 @@ void LoadBoxes()
int excessiveZoneGroups = numZoneGroups - j + 1;
TENLog("Level file contains extra pathfinding data, number of excessive zone groups is " +
std::to_string(excessiveZoneGroups) + ". These zone groups will be ignored.", LogLevel::Warning);
LevelDataPtr += numBoxes * sizeof(int);
CurrentDataPtr += numBoxes * sizeof(int);
}
else
{
@ -1406,13 +1496,35 @@ void LoadBoxes()
bool LoadLevelFile(int levelIndex)
{
TENLog("Loading level file...", LogLevel::Info);
const auto& level = *g_GameFlow->GetLevel(levelIndex);
auto assetDir = g_GameFlow->GetGameDir();
auto levelPath = assetDir + level.FileName;
if (!std::filesystem::is_regular_file(levelPath))
{
TENLog("Level file not found: " + levelPath, LogLevel::Error);
return false;
}
TENLog("Loading level file: " + levelPath, LogLevel::Info);
auto timestamp = std::filesystem::last_write_time(levelPath);
bool fastReload = (g_GameFlow->GetSettings()->FastReload && levelIndex == CurrentLevel && timestamp == LastLevelTimestamp && levelPath == LastLevelFilePath);
// Dumping game scene right after engine launch is impossible, as no scene exists.
if (!FirstLevel && fastReload)
g_Renderer.DumpGameScene();
auto loadingScreenPath = TEN::Utils::ToWString(assetDir + level.LoadScreenFileName);
g_Renderer.SetLoadingScreen(fastReload ? std::wstring{} : loadingScreenPath);
BackupLara();
StopAllSounds();
CleanUp();
FreeLevel();
FreeLevel(fastReload);
LevelLoadTask = std::async(std::launch::async, LoadLevel, levelIndex);
LevelLoadTask = std::async(std::launch::async, LoadLevel, levelPath, fastReload);
return LevelLoadTask.get();
}

View file

@ -140,6 +140,8 @@ extern std::vector<int> MoveablesIds;
extern std::vector<int> StaticObjectsIds;
extern std::vector<int> SpriteSequencesIds;
extern LEVEL g_Level;
extern int SystemNameHash;
extern int LastLevelHash;
inline std::future<bool> LevelLoadTask;
@ -149,7 +151,7 @@ void FileClose(FILE* ptr);
bool Decompress(byte* dest, byte* src, unsigned long compressedSize, unsigned long uncompressedSize);
bool LoadLevelFile(int levelIndex);
void FreeLevel();
void FreeLevel(bool partial);
void LoadTextures();
void LoadRooms();

View file

@ -7017,6 +7017,7 @@ flatbuffers::Offset<UnionVec> CreateUnionVec(flatbuffers::FlatBufferBuilder &_fb
struct SaveGameHeaderT : public flatbuffers::NativeTable {
typedef SaveGameHeader TableType;
std::string level_name{};
int32_t level_hash = 0;
int32_t days = 0;
int32_t hours = 0;
int32_t minutes = 0;
@ -7032,17 +7033,21 @@ struct SaveGameHeader FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
struct Traits;
enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
VT_LEVEL_NAME = 4,
VT_DAYS = 6,
VT_HOURS = 8,
VT_MINUTES = 10,
VT_SECONDS = 12,
VT_LEVEL = 14,
VT_TIMER = 16,
VT_COUNT = 18
VT_LEVEL_HASH = 6,
VT_DAYS = 8,
VT_HOURS = 10,
VT_MINUTES = 12,
VT_SECONDS = 14,
VT_LEVEL = 16,
VT_TIMER = 18,
VT_COUNT = 20
};
const flatbuffers::String *level_name() const {
return GetPointer<const flatbuffers::String *>(VT_LEVEL_NAME);
}
int32_t level_hash() const {
return GetField<int32_t>(VT_LEVEL_HASH, 0);
}
int32_t days() const {
return GetField<int32_t>(VT_DAYS, 0);
}
@ -7068,6 +7073,7 @@ struct SaveGameHeader FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
return VerifyTableStart(verifier) &&
VerifyOffset(verifier, VT_LEVEL_NAME) &&
verifier.VerifyString(level_name()) &&
VerifyField<int32_t>(verifier, VT_LEVEL_HASH) &&
VerifyField<int32_t>(verifier, VT_DAYS) &&
VerifyField<int32_t>(verifier, VT_HOURS) &&
VerifyField<int32_t>(verifier, VT_MINUTES) &&
@ -7089,6 +7095,9 @@ struct SaveGameHeaderBuilder {
void add_level_name(flatbuffers::Offset<flatbuffers::String> level_name) {
fbb_.AddOffset(SaveGameHeader::VT_LEVEL_NAME, level_name);
}
void add_level_hash(int32_t level_hash) {
fbb_.AddElement<int32_t>(SaveGameHeader::VT_LEVEL_HASH, level_hash, 0);
}
void add_days(int32_t days) {
fbb_.AddElement<int32_t>(SaveGameHeader::VT_DAYS, days, 0);
}
@ -7124,6 +7133,7 @@ struct SaveGameHeaderBuilder {
inline flatbuffers::Offset<SaveGameHeader> CreateSaveGameHeader(
flatbuffers::FlatBufferBuilder &_fbb,
flatbuffers::Offset<flatbuffers::String> level_name = 0,
int32_t level_hash = 0,
int32_t days = 0,
int32_t hours = 0,
int32_t minutes = 0,
@ -7139,6 +7149,7 @@ inline flatbuffers::Offset<SaveGameHeader> CreateSaveGameHeader(
builder_.add_minutes(minutes);
builder_.add_hours(hours);
builder_.add_days(days);
builder_.add_level_hash(level_hash);
builder_.add_level_name(level_name);
return builder_.Finish();
}
@ -7151,6 +7162,7 @@ struct SaveGameHeader::Traits {
inline flatbuffers::Offset<SaveGameHeader> CreateSaveGameHeaderDirect(
flatbuffers::FlatBufferBuilder &_fbb,
const char *level_name = nullptr,
int32_t level_hash = 0,
int32_t days = 0,
int32_t hours = 0,
int32_t minutes = 0,
@ -7162,6 +7174,7 @@ inline flatbuffers::Offset<SaveGameHeader> CreateSaveGameHeaderDirect(
return TEN::Save::CreateSaveGameHeader(
_fbb,
level_name__,
level_hash,
days,
hours,
minutes,
@ -10245,6 +10258,7 @@ inline void SaveGameHeader::UnPackTo(SaveGameHeaderT *_o, const flatbuffers::res
(void)_o;
(void)_resolver;
{ auto _e = level_name(); if (_e) _o->level_name = _e->str(); }
{ auto _e = level_hash(); _o->level_hash = _e; }
{ auto _e = days(); _o->days = _e; }
{ auto _e = hours(); _o->hours = _e; }
{ auto _e = minutes(); _o->minutes = _e; }
@ -10263,6 +10277,7 @@ inline flatbuffers::Offset<SaveGameHeader> CreateSaveGameHeader(flatbuffers::Fla
(void)_o;
struct _VectorArgs { flatbuffers::FlatBufferBuilder *__fbb; const SaveGameHeaderT* __o; const flatbuffers::rehasher_function_t *__rehasher; } _va = { &_fbb, _o, _rehasher}; (void)_va;
auto _level_name = _o->level_name.empty() ? _fbb.CreateSharedString("") : _fbb.CreateString(_o->level_name);
auto _level_hash = _o->level_hash;
auto _days = _o->days;
auto _hours = _o->hours;
auto _minutes = _o->minutes;
@ -10273,6 +10288,7 @@ inline flatbuffers::Offset<SaveGameHeader> CreateSaveGameHeader(flatbuffers::Fla
return TEN::Save::CreateSaveGameHeader(
_fbb,
_level_name,
_level_hash,
_days,
_hours,
_minutes,

View file

@ -504,6 +504,7 @@ table UnionVec {
table SaveGameHeader {
level_name: string;
level_hash: int32;
days: int32;
hours: int32;
minutes: int32;

View file

@ -4,6 +4,7 @@
#include <array>
#include <d3d11.h>
#include <deque>
#include <filesystem>
#include <functional>
#include <future>
#include <map>