/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com.                */
#include <base/bytes.h>
#include <base/dbg.h>
#include <base/fs.h>
#include <base/io.h>
#include <base/log.h>
#include <base/math.h>
#include <base/mem.h>
#include <base/str.h>
#include <base/time.h>

#include <engine/console.h>
#include <engine/shared/config.h>
#include <engine/storage.h>

#if defined(CONF_VIDEORECORDER)
#include <engine/shared/video.h>
#endif

#include "compression.h"
#include "demo.h"
#include "network.h"
#include "snapshot.h"

const CUuid SHA256_EXTENSION =
	{{0x6b, 0xe6, 0xda, 0x4a, 0xce, 0xbd, 0x38, 0x0c,
		0x9b, 0x5b, 0x12, 0x89, 0xc8, 0x42, 0xd7, 0x80}};

static const unsigned char gs_CurVersion = 6;
static const unsigned char gs_OldVersion = 3;
static const unsigned char gs_Sha256Version = 6;
static const unsigned char gs_VersionTickCompression = 5; // demo files with this version or higher will use `CHUNKTICKFLAG_TICK_COMPRESSED`

// TODO: rewrite all logs in this file using log_log_color, and remove gs_DemoPrintColor and m_pConsole
static constexpr ColorRGBA gs_DemoPrintColor{0.75f, 0.7f, 0.7f, 1.0f};
static constexpr LOG_COLOR DEMO_PRINT_COLOR = {191, 178, 178};

bool CDemoHeader::Valid() const
{
	// Check marker and ensure that strings are zero-terminated and valid UTF-8.
	return mem_comp(m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) == 0 &&
	       mem_has_null(m_aNetversion, sizeof(m_aNetversion)) && str_utf8_check(m_aNetversion) &&
	       mem_has_null(m_aMapName, sizeof(m_aMapName)) && str_utf8_check(m_aMapName) &&
	       mem_has_null(m_aType, sizeof(m_aType)) && str_utf8_check(m_aType) &&
	       mem_has_null(m_aTimestamp, sizeof(m_aTimestamp)) && str_utf8_check(m_aTimestamp);
}

CDemoRecorder::CDemoRecorder(CSnapshotDelta *pSnapshotDelta, bool NoMapData)
{
	m_File = nullptr;
	m_aCurrentFilename[0] = '\0';
	m_pfnFilter = nullptr;
	m_pUser = nullptr;
	m_LastTickMarker = -1;
	m_pSnapshotDelta = pSnapshotDelta;
	m_NoMapData = NoMapData;
}

CDemoRecorder::~CDemoRecorder()
{
	dbg_assert(m_File == nullptr, "Demo recorder was not stopped");
}

// Record
int CDemoRecorder::Start(IStorage *pStorage, IConsole *pConsole, const char *pFilename, const char *pNetVersion, const char *pMap, const SHA256_DIGEST &Sha256, unsigned Crc, const char *pType, unsigned MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser)
{
	dbg_assert(m_File == nullptr, "Demo recorder already recording");

	m_pConsole = pConsole;
	m_pStorage = pStorage;

	if(!str_valid_filename(fs_filename(pFilename)))
	{
		log_error_color(DEMO_PRINT_COLOR, "demo_recorder", "The name '%s' cannot be used for demos because not all platforms support it", pFilename);
		return -1;
	}

	IOHANDLE DemoFile = pStorage->OpenFile(pFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
	if(!DemoFile)
	{
		if(m_pConsole)
		{
			char aBuf[64 + IO_MAX_PATH_LENGTH];
			str_format(aBuf, sizeof(aBuf), "Unable to open '%s' for recording", pFilename);
			m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor);
		}
		return -1;
	}

	bool CloseMapFile = false;

	if(MapFile)
		io_seek(MapFile, 0, EIoSeekOrigin::START);

	char aSha256[SHA256_MAXSTRSIZE];
	sha256_str(Sha256, aSha256, sizeof(aSha256));

	if(!pMapData && !MapFile)
	{
		// open mapfile
		char aMapFilename[IO_MAX_PATH_LENGTH];
		// try the downloaded maps
		str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%s.map", pMap, aSha256);
		MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_READ, IStorage::TYPE_ALL);
		if(!MapFile)
		{
			// try the normal maps folder
			str_format(aMapFilename, sizeof(aMapFilename), "maps/%s.map", pMap);
			MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_READ, IStorage::TYPE_ALL);
		}
		if(!MapFile)
		{
			// search for the map within subfolders
			char aBuf[IO_MAX_PATH_LENGTH];
			str_format(aMapFilename, sizeof(aMapFilename), "%s.map", pMap);
			if(pStorage->FindFile(aMapFilename, "maps", IStorage::TYPE_ALL, aBuf, sizeof(aBuf)))
				MapFile = pStorage->OpenFile(aBuf, IOFLAG_READ, IStorage::TYPE_ALL);
		}
		if(!MapFile)
		{
			if(m_pConsole)
			{
				char aBuf[32 + IO_MAX_PATH_LENGTH];
				str_format(aBuf, sizeof(aBuf), "Unable to open mapfile '%s'", pMap);
				m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor);
			}
			return -1;
		}

		CloseMapFile = true;
	}

	if(m_NoMapData)
	{
		MapSize = 0;
	}
	else if(MapFile)
	{
		const int64_t MapFileSize = io_length(MapFile);
		if(MapFileSize > (int64_t)std::numeric_limits<unsigned>::max())
		{
			if(CloseMapFile)
			{
				io_close(MapFile);
			}
			MapSize = 0;
			if(m_pConsole)
			{
				char aBuf[32 + IO_MAX_PATH_LENGTH];
				str_format(aBuf, sizeof(aBuf), "Mapfile '%s' too large for demo, recording without it", pMap);
				m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor);
			}
		}
		else
		{
			MapSize = MapFileSize;
		}
	}

	// write header
	CDemoHeader Header;
	mem_zero(&Header, sizeof(Header));
	mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker));
	Header.m_Version = gs_CurVersion;
	str_copy(Header.m_aNetversion, pNetVersion);
	str_copy(Header.m_aMapName, pMap);
	uint_to_bytes_be(Header.m_aMapSize, MapSize);
	uint_to_bytes_be(Header.m_aMapCrc, Crc);
	str_copy(Header.m_aType, pType);
	// Header.m_Length - add this on stop
	str_timestamp(Header.m_aTimestamp, sizeof(Header.m_aTimestamp));
	io_write(DemoFile, &Header, sizeof(Header));

	CTimelineMarkers TimelineMarkers;
	mem_zero(&TimelineMarkers, sizeof(TimelineMarkers));
	io_write(DemoFile, &TimelineMarkers, sizeof(TimelineMarkers)); // fill this on stop

	// Write Sha256
	io_write(DemoFile, SHA256_EXTENSION.m_aData, sizeof(SHA256_EXTENSION.m_aData));
	io_write(DemoFile, &Sha256, sizeof(SHA256_DIGEST));

	if(MapSize == 0)
	{
	}
	else if(pMapData)
	{
		io_write(DemoFile, pMapData, MapSize);
	}
	else
	{
		// write map data
		while(true)
		{
			unsigned char aChunk[1024 * 64];
			int Bytes = io_read(MapFile, &aChunk, sizeof(aChunk));
			if(Bytes <= 0)
				break;
			io_write(DemoFile, &aChunk, Bytes);
		}
		if(CloseMapFile)
			io_close(MapFile);
		else
			io_seek(MapFile, 0, EIoSeekOrigin::START);
	}

	m_LastKeyFrame = -1;
	m_LastTickMarker = -1;
	m_FirstTick = -1;
	m_NumTimelineMarkers = 0;

	if(m_pConsole)
	{
		char aBuf[32 + IO_MAX_PATH_LENGTH];
		str_format(aBuf, sizeof(aBuf), "Recording to '%s'", pFilename);
		m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor);
	}

	m_pfnFilter = pfnFilter;
	m_pUser = pUser;

	m_File = DemoFile;
	str_copy(m_aCurrentFilename, pFilename);

	return 0;
}

/*
	Tickmarker
		7	= Always set
		6	= Keyframe flag
		0-5	= Delta tick

	Normal
		7 = Not set
		5-6	= Type
		0-4	= Size
*/

enum
{
	CHUNKTYPEFLAG_TICKMARKER = 0x80,
	CHUNKTICKFLAG_KEYFRAME = 0x40, // only when tickmarker is set
	CHUNKTICKFLAG_TICK_COMPRESSED = 0x20, // when we store the tick value in the first chunk

	CHUNKMASK_TICK = 0x1f,
	CHUNKMASK_TICK_LEGACY = 0x3f,
	CHUNKMASK_TYPE = 0x60,
	CHUNKMASK_SIZE = 0x1f,

	CHUNKTYPE_SNAPSHOT = 1,
	CHUNKTYPE_MESSAGE = 2,
	CHUNKTYPE_DELTA = 3,
};

void CDemoRecorder::WriteTickMarker(int Tick, bool Keyframe)
{
	if(m_LastTickMarker == -1 || Tick - m_LastTickMarker > CHUNKMASK_TICK || Keyframe)
	{
		unsigned char aChunk[sizeof(int32_t) + 1];
		aChunk[0] = CHUNKTYPEFLAG_TICKMARKER;
		uint_to_bytes_be(aChunk + 1, Tick);

		if(Keyframe)
			aChunk[0] |= CHUNKTICKFLAG_KEYFRAME;

		io_write(m_File, aChunk, sizeof(aChunk));
	}
	else
	{
		unsigned char aChunk[1];
		aChunk[0] = CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_TICK_COMPRESSED | (Tick - m_LastTickMarker);
		io_write(m_File, aChunk, sizeof(aChunk));
	}

	m_LastTickMarker = Tick;
	if(m_FirstTick < 0)
		m_FirstTick = Tick;
}

void CDemoRecorder::Write(int Type, const void *pData, int Size)
{
	if(!m_File)
		return;

	if(Size > 64 * 1024)
		return;

	/* pad the data with 0 so we get an alignment of 4,
	else the compression won't work and miss some bytes */
	char aBuffer[64 * 1024];
	char aBuffer2[64 * 1024];
	mem_copy(aBuffer2, pData, Size);
	while(Size & 3)
		aBuffer2[Size++] = 0;
	Size = CVariableInt::Compress(aBuffer2, Size, aBuffer, sizeof(aBuffer)); // buffer2 -> buffer
	if(Size < 0)
		return;

	Size = CNetBase::Compress(aBuffer, Size, aBuffer2, sizeof(aBuffer2)); // buffer -> buffer2
	if(Size < 0)
		return;

	unsigned char aChunk[3];
	aChunk[0] = ((Type & 0x3) << 5);
	if(Size < 30)
	{
		aChunk[0] |= Size;
		io_write(m_File, aChunk, 1);
	}
	else
	{
		if(Size < 256)
		{
			aChunk[0] |= 30;
			aChunk[1] = Size & 0xff;
			io_write(m_File, aChunk, 2);
		}
		else
		{
			aChunk[0] |= 31;
			aChunk[1] = Size & 0xff;
			aChunk[2] = Size >> 8;
			io_write(m_File, aChunk, 3);
		}
	}

	io_write(m_File, aBuffer2, Size);
}

void CDemoRecorder::RecordSnapshot(int Tick, const void *pData, int Size)
{
	if(m_LastKeyFrame == -1 || (Tick - m_LastKeyFrame) > SERVER_TICK_SPEED * 5)
	{
		// write full tickmarker
		WriteTickMarker(Tick, true);

		// write snapshot
		Write(CHUNKTYPE_SNAPSHOT, pData, Size);

		m_LastKeyFrame = Tick;
		mem_copy(&m_LastSnapshotData, pData, Size);
	}
	else
	{
		// write tickmarker
		WriteTickMarker(Tick, false);

		// create delta
		char aDeltaData[CSnapshot::MAX_SIZE];
		const int DeltaSize = m_pSnapshotDelta->CreateDelta(m_LastSnapshotData.AsSnapshot(), (CSnapshot *)pData, &aDeltaData);
		if(DeltaSize)
		{
			// record delta
			Write(CHUNKTYPE_DELTA, aDeltaData, DeltaSize);
			mem_copy(&m_LastSnapshotData, pData, Size);
		}
	}
}

void CDemoRecorder::RecordMessage(const void *pData, int Size)
{
	if(m_pfnFilter)
	{
		if(m_pfnFilter(pData, Size, m_pUser))
		{
			return;
		}
	}
	Write(CHUNKTYPE_MESSAGE, pData, Size);
}

int CDemoRecorder::Stop(IDemoRecorder::EStopMode Mode, const char *pTargetFilename)
{
	if(!m_File)
		return -1;

	if(Mode == IDemoRecorder::EStopMode::KEEP_FILE)
	{
		// add the demo length to the header
		io_seek(m_File, offsetof(CDemoHeader, m_aLength), EIoSeekOrigin::START);
		unsigned char aLength[sizeof(int32_t)];
		uint_to_bytes_be(aLength, Length());
		io_write(m_File, aLength, sizeof(aLength));

		// add the timeline markers to the header
		io_seek(m_File, sizeof(CDemoHeader) + offsetof(CTimelineMarkers, m_aNumTimelineMarkers), EIoSeekOrigin::START);
		unsigned char aNumMarkers[sizeof(int32_t)];
		uint_to_bytes_be(aNumMarkers, m_NumTimelineMarkers);
		io_write(m_File, aNumMarkers, sizeof(aNumMarkers));
		for(int i = 0; i < m_NumTimelineMarkers; i++)
		{
			unsigned char aMarker[sizeof(int32_t)];
			uint_to_bytes_be(aMarker, m_aTimelineMarkers[i]);
			io_write(m_File, aMarker, sizeof(aMarker));
		}
	}

	io_close(m_File);
	m_File = nullptr;

	if(Mode == IDemoRecorder::EStopMode::REMOVE_FILE)
	{
		if(!m_pStorage->RemoveFile(m_aCurrentFilename, IStorage::TYPE_SAVE))
		{
			if(m_pConsole)
			{
				char aBuf[64 + IO_MAX_PATH_LENGTH];
				str_format(aBuf, sizeof(aBuf), "Could not remove demo file '%s'.", m_aCurrentFilename);
				m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor);
			}
			return -1;
		}
	}
	else if(pTargetFilename[0] != '\0')
	{
		if(!m_pStorage->RenameFile(m_aCurrentFilename, pTargetFilename, IStorage::TYPE_SAVE))
		{
			if(m_pConsole)
			{
				char aBuf[64 + 2 * IO_MAX_PATH_LENGTH];
				str_format(aBuf, sizeof(aBuf), "Could not move demo file '%s' to '%s'.", m_aCurrentFilename, pTargetFilename);
				m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor);
			}
			return -1;
		}
	}

	if(m_pConsole)
	{
		char aBuf[64 + IO_MAX_PATH_LENGTH];
		str_format(aBuf, sizeof(aBuf), "Stopped recording to '%s'", m_aCurrentFilename);
		m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor);
	}

	return 0;
}

void CDemoRecorder::AddDemoMarker()
{
	if(m_LastTickMarker < 0)
		return;
	AddDemoMarker(m_LastTickMarker);
}

void CDemoRecorder::AddDemoMarker(int Tick)
{
	dbg_assert(Tick >= 0, "invalid marker tick");
	if(m_NumTimelineMarkers >= MAX_TIMELINE_MARKERS)
	{
		if(m_pConsole)
		{
			m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Too many timeline markers", gs_DemoPrintColor);
		}
		return;
	}

	// not more than 1 marker in a second
	if(m_NumTimelineMarkers > 0)
	{
		const int Diff = Tick - m_aTimelineMarkers[m_NumTimelineMarkers - 1];
		if(Diff < (float)SERVER_TICK_SPEED)
		{
			if(m_pConsole)
			{
				m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Previous timeline marker too close", gs_DemoPrintColor);
			}
			return;
		}
	}

	m_aTimelineMarkers[m_NumTimelineMarkers++] = Tick;

	if(m_pConsole)
	{
		m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Added timeline marker", gs_DemoPrintColor);
	}
}

CSnapshotDelta *CDemoPlayer::SnapshotDelta()
{
	if(IsSixup())
	{
		return m_pSnapshotDeltaSixup;
	}
	return m_pSnapshotDelta;
}

void CDemoPlayer::Construct(CSnapshotDelta *pSnapshotDelta, CSnapshotDelta *pSnapshotDeltaSixup, bool UseVideo)
{
	m_File = nullptr;
	m_SpeedIndex = DEMO_SPEED_INDEX_DEFAULT;

	m_pSnapshotDelta = pSnapshotDelta;
	m_pSnapshotDeltaSixup = pSnapshotDeltaSixup;
	m_LastSnapshotDataSize = -1;
	m_pListener = nullptr;
	m_UseVideo = UseVideo;

	m_aFilename[0] = '\0';
	m_aErrorMessage[0] = '\0';
}

CDemoPlayer::CDemoPlayer(CSnapshotDelta *pSnapshotDelta, CSnapshotDelta *pSnapshotDeltaSixup, bool UseVideo, TUpdateIntraTimesFunc &&UpdateIntraTimesFunc)
{
	Construct(pSnapshotDelta, pSnapshotDeltaSixup, UseVideo);

	m_UpdateIntraTimesFunc = UpdateIntraTimesFunc;
}

CDemoPlayer::CDemoPlayer(CSnapshotDelta *pSnapshotDelta, CSnapshotDelta *pSnapshotDeltaSixup, bool UseVideo)
{
	Construct(pSnapshotDelta, pSnapshotDeltaSixup, UseVideo);
}

CDemoPlayer::~CDemoPlayer()
{
	dbg_assert(m_File == nullptr, "Demo player not stopped");
}

void CDemoPlayer::SetListener(IListener *pListener)
{
	m_pListener = pListener;
}

CDemoPlayer::EReadChunkHeaderResult CDemoPlayer::ReadChunkHeader(int *pType, int *pSize, int *pTick)
{
	*pSize = 0;
	*pType = 0;

	unsigned char Chunk = 0;
	if(io_read(m_File, &Chunk, sizeof(Chunk)) != sizeof(Chunk))
		return CHUNKHEADER_EOF;

	if(Chunk & CHUNKTYPEFLAG_TICKMARKER)
	{
		// decode tick marker
		int TickdeltaLegacy = Chunk & CHUNKMASK_TICK_LEGACY; // compatibility
		*pType = Chunk & (CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_KEYFRAME);

		int NewTick;
		if(m_Info.m_Header.m_Version < gs_VersionTickCompression && TickdeltaLegacy != 0)
		{
			if(*pTick < 0) // initial tick not initialized before a tick delta
				return CHUNKHEADER_ERROR;
			NewTick = *pTick + TickdeltaLegacy;
		}
		else if(Chunk & CHUNKTICKFLAG_TICK_COMPRESSED)
		{
			if(*pTick < 0) // initial tick not initialized before a tick delta
				return CHUNKHEADER_ERROR;
			int Tickdelta = Chunk & CHUNKMASK_TICK;
			NewTick = *pTick + Tickdelta;
		}
		else
		{
			unsigned char aTickdata[sizeof(int32_t)];
			if(io_read(m_File, aTickdata, sizeof(aTickdata)) != sizeof(aTickdata))
				return CHUNKHEADER_ERROR;
			NewTick = bytes_be_to_uint(aTickdata);
		}
		if(NewTick < MIN_TICK || NewTick >= MAX_TICK) // invalid tick
			return CHUNKHEADER_ERROR;
		*pTick = NewTick;
	}
	else
	{
		// decode normal chunk
		*pType = (Chunk & CHUNKMASK_TYPE) >> 5;
		*pSize = Chunk & CHUNKMASK_SIZE;

		if(*pSize == 30)
		{
			unsigned char aSizedata[1];
			if(io_read(m_File, aSizedata, sizeof(aSizedata)) != sizeof(aSizedata))
				return CHUNKHEADER_ERROR;
			*pSize = aSizedata[0];
		}
		else if(*pSize == 31)
		{
			unsigned char aSizedata[2];
			if(io_read(m_File, aSizedata, sizeof(aSizedata)) != sizeof(aSizedata))
				return CHUNKHEADER_ERROR;
			*pSize = (aSizedata[1] << 8) | aSizedata[0];
		}
	}

	return CHUNKHEADER_SUCCESS;
}

CDemoPlayer::EScanFileResult CDemoPlayer::ScanFile()
{
	const int64_t StartPos = io_tell(m_File);
	if(StartPos < 0)
	{
		return EScanFileResult::ERROR_UNRECOVERABLE;
	}

	const auto &ResetToStartPosition = [&](EScanFileResult Result) -> EScanFileResult {
		if(io_seek(m_File, StartPos, EIoSeekOrigin::START) != 0)
		{
			m_vKeyFrames.clear();
			return EScanFileResult::ERROR_UNRECOVERABLE;
		}
		return Result;
	};

	int ChunkTick = -1;
	if(!m_vKeyFrames.empty())
	{
		if(io_seek(m_File, m_vKeyFrames.back().m_Filepos, EIoSeekOrigin::START) != 0)
		{
			return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
		}
		int ChunkType, ChunkSize;
		const EReadChunkHeaderResult Result = ReadChunkHeader(&ChunkType, &ChunkSize, &ChunkTick);
		if(Result != CHUNKHEADER_SUCCESS ||
			(ChunkSize > 0 && io_skip(m_File, ChunkSize) != 0))
		{
			return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
		}
	}

	while(true)
	{
		const int64_t CurrentPos = io_tell(m_File);
		if(CurrentPos < 0)
		{
			return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
		}

		int ChunkType, ChunkSize;
		const EReadChunkHeaderResult Result = ReadChunkHeader(&ChunkType, &ChunkSize, &ChunkTick);
		if(Result == CHUNKHEADER_EOF)
		{
			break;
		}
		else if(Result == CHUNKHEADER_ERROR)
		{
			return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
		}

		if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
		{
			if(ChunkType & CHUNKTICKFLAG_KEYFRAME)
			{
				m_vKeyFrames.emplace_back(CurrentPos, ChunkTick);
			}
			if(m_Info.m_Info.m_FirstTick == -1)
			{
				m_Info.m_Info.m_FirstTick = ChunkTick;
			}
			m_Info.m_Info.m_LastTick = ChunkTick;
		}
		else if(ChunkSize)
		{
			if(io_skip(m_File, ChunkSize) != 0)
			{
				return ResetToStartPosition(EScanFileResult::ERROR_RECOVERABLE);
			}
		}
	}

	// Cannot start playback without at least one keyframe
	return ResetToStartPosition(m_vKeyFrames.empty() ? EScanFileResult::ERROR_UNRECOVERABLE : EScanFileResult::SUCCESS);
}

void CDemoPlayer::DoTick()
{
	// update ticks
	m_Info.m_PreviousTick = m_Info.m_Info.m_CurrentTick;
	m_Info.m_Info.m_CurrentTick = m_Info.m_NextTick;
	int ChunkTick = m_Info.m_Info.m_CurrentTick;

	UpdateTimes();

	bool GotSnapshot = false;
	while(true)
	{
		int ChunkType, ChunkSize;
		const EReadChunkHeaderResult Result = ReadChunkHeader(&ChunkType, &ChunkSize, &ChunkTick);
		if(Result == CHUNKHEADER_EOF)
		{
			if(m_Info.m_PreviousTick == -1)
			{
				Stop("Empty demo");
			}
			else
			{
				Pause();
				// Stop rendering when reaching end of file
#if defined(CONF_VIDEORECORDER)
				if(m_UseVideo && IVideo::Current())
					Stop();
#endif
			}
			break;
		}
		else if(Result == CHUNKHEADER_ERROR)
		{
			Stop("Error reading chunk header");
			break;
		}

		// read the chunk
		int DataSize = 0;
		if(ChunkSize)
		{
			if(io_read(m_File, m_aCompressedSnapshotData, ChunkSize) != (unsigned)ChunkSize)
			{
				Stop("Error reading chunk data");
				break;
			}

			DataSize = CNetBase::Decompress(m_aCompressedSnapshotData, ChunkSize, m_aDecompressedSnapshotData, sizeof(m_aDecompressedSnapshotData));
			if(DataSize < 0)
			{
				Stop("Error during network decompression");
				break;
			}

			DataSize = CVariableInt::Decompress(m_aDecompressedSnapshotData, DataSize, m_aChunkData, sizeof(m_aChunkData));
			if(DataSize < 0)
			{
				Stop("Error during intpack decompression");
				break;
			}
		}

		if(ChunkType == CHUNKTYPE_DELTA)
		{
			if(m_LastSnapshotDataSize == -1)
			{
				Stop("Delta snapshot before any full snapshot");
				break;
			}

			// process delta snapshot
			DataSize = SnapshotDelta()->UnpackDelta(m_LastSnapshotData.AsSnapshot(), &m_Snapshot, m_aChunkData, DataSize);

			if(DataSize < 0)
			{
				if(m_pConsole)
				{
					char aBuf[64];
					str_format(aBuf, sizeof(aBuf), "Error unpacking snapshot delta. DataSize=%d", DataSize);
					m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", aBuf);
				}
			}
			else if(!m_Snapshot.AsSnapshot()->IsValid(DataSize))
			{
				if(m_pConsole)
				{
					char aBuf[64];
					str_format(aBuf, sizeof(aBuf), "Snapshot delta invalid. DataSize=%d", DataSize);
					m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", aBuf);
				}
			}
			else
			{
				if(m_pListener)
					m_pListener->OnDemoPlayerSnapshot(m_Snapshot.AsSnapshot(), DataSize);

				m_LastSnapshotDataSize = DataSize;
				mem_copy(&m_LastSnapshotData, &m_Snapshot, DataSize);
				GotSnapshot = true;
			}
		}
		else if(ChunkType == CHUNKTYPE_SNAPSHOT)
		{
			// process full snapshot
			CSnapshot *pSnap = (CSnapshot *)m_aChunkData;
			if(!pSnap->IsValid(DataSize))
			{
				if(m_pConsole)
				{
					char aBuf[64];
					str_format(aBuf, sizeof(aBuf), "Snapshot invalid. DataSize=%d", DataSize);
					m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", aBuf);
				}
			}
			else
			{
				GotSnapshot = true;

				m_LastSnapshotDataSize = DataSize;
				mem_copy(&m_LastSnapshotData, m_aChunkData, DataSize);
				if(m_pListener)
					m_pListener->OnDemoPlayerSnapshot(m_aChunkData, DataSize);
			}
		}
		else
		{
			// if there were no snapshots in this tick, replay the last one
			if(!GotSnapshot && m_pListener && m_LastSnapshotDataSize != -1)
			{
				GotSnapshot = true;
				m_pListener->OnDemoPlayerSnapshot(&m_LastSnapshotData, m_LastSnapshotDataSize);
			}

			// check the remaining types
			if(ChunkType & CHUNKTYPEFLAG_TICKMARKER)
			{
				m_Info.m_NextTick = ChunkTick;
				break;
			}
			else if(ChunkType == CHUNKTYPE_MESSAGE)
			{
				if(m_pListener)
					m_pListener->OnDemoPlayerMessage(m_aChunkData, DataSize);
			}
		}
	}
}

void CDemoPlayer::Pause()
{
	m_Info.m_Info.m_Paused = true;
#if defined(CONF_VIDEORECORDER)
	if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
		IVideo::Current()->Pause(true);
#endif
}

void CDemoPlayer::Unpause()
{
	m_Info.m_Info.m_Paused = false;
#if defined(CONF_VIDEORECORDER)
	if(m_UseVideo && IVideo::Current() && g_Config.m_ClVideoPauseWithDemo)
		IVideo::Current()->Pause(false);
#endif
}

int CDemoPlayer::Load(IStorage *pStorage, IConsole *pConsole, const char *pFilename, int StorageType)
{
	dbg_assert(m_File == nullptr, "Demo player already playing");

	m_pConsole = pConsole;
	str_copy(m_aFilename, pFilename);
	str_copy(m_aErrorMessage, "");

	if(m_pConsole)
	{
		char aBuf[32 + IO_MAX_PATH_LENGTH];
		str_format(aBuf, sizeof(aBuf), "Loading demo '%s'", pFilename);
		m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", aBuf);
	}

	// clear the playback info
	mem_zero(&m_Info, sizeof(m_Info));
	m_Info.m_Info.m_FirstTick = -1;
	m_Info.m_Info.m_LastTick = -1;
	m_Info.m_NextTick = -1;
	m_Info.m_Info.m_CurrentTick = -1;
	m_Info.m_PreviousTick = -1;
	m_Info.m_Info.m_Speed = 1;
	m_SpeedIndex = DEMO_SPEED_INDEX_DEFAULT;
	m_LastSnapshotDataSize = -1;

	if(!GetDemoInfo(pStorage, m_pConsole, pFilename, StorageType, &m_Info.m_Header, &m_Info.m_TimelineMarkers, &m_MapInfo, &m_File, m_aErrorMessage, sizeof(m_aErrorMessage)))
	{
		str_copy(m_aFilename, "");
		return -1;
	}
	m_Sixup = str_startswith(m_Info.m_Header.m_aNetversion, "0.7");

	// save byte offset of map for later use
	m_MapOffset = io_tell(m_File);
	if(m_MapOffset < 0 || io_skip(m_File, m_MapInfo.m_Size) != 0)
	{
		Stop("Error skipping map data");
		return -1;
	}

	if(m_Info.m_Header.m_Version > gs_OldVersion)
	{
		// get timeline markers
		int Num = bytes_be_to_uint(m_Info.m_TimelineMarkers.m_aNumTimelineMarkers);
		m_Info.m_Info.m_NumTimelineMarkers = std::clamp<int>(Num, 0, MAX_TIMELINE_MARKERS);
		for(int i = 0; i < m_Info.m_Info.m_NumTimelineMarkers; i++)
		{
			m_Info.m_Info.m_aTimelineMarkers[i] = bytes_be_to_uint(m_Info.m_TimelineMarkers.m_aTimelineMarkers[i]);
		}
	}

	// Scan the file for interesting points
	if(ScanFile() == EScanFileResult::ERROR_UNRECOVERABLE)
	{
		Stop("Error scanning demo file");
		return -1;
	}
	m_Info.m_LiveStateUpdating = true;

	// reset slice markers
	g_Config.m_ClDemoSliceBegin = -1;
	g_Config.m_ClDemoSliceEnd = -1;

	// ready for playback
	return 0;
}

unsigned char *CDemoPlayer::GetMapData(IStorage *pStorage)
{
	if(!m_MapInfo.m_Size)
		return nullptr;

	const int64_t CurSeek = io_tell(m_File);
	if(CurSeek < 0 || io_seek(m_File, m_MapOffset, EIoSeekOrigin::START) != 0)
		return nullptr;
	unsigned char *pMapData = (unsigned char *)malloc(m_MapInfo.m_Size);
	if(io_read(m_File, pMapData, m_MapInfo.m_Size) != m_MapInfo.m_Size ||
		io_seek(m_File, CurSeek, EIoSeekOrigin::START) != 0)
	{
		free(pMapData);
		return nullptr;
	}
	return pMapData;
}

bool CDemoPlayer::ExtractMap(IStorage *pStorage)
{
	unsigned char *pMapData = GetMapData(pStorage);
	if(!pMapData)
		return false;

	// handle sha256
	std::optional<SHA256_DIGEST> Sha256;
	if(m_Info.m_Header.m_Version >= gs_Sha256Version)
	{
		Sha256 = m_MapInfo.m_Sha256;
		dbg_assert(Sha256.has_value(), "SHA256 missing for version %d demo", m_Info.m_Header.m_Version);
	}
	else
	{
		Sha256 = sha256(pMapData, m_MapInfo.m_Size);
		m_MapInfo.m_Sha256 = Sha256;
	}

	// construct name
	char aSha[SHA256_MAXSTRSIZE], aMapFilename[IO_MAX_PATH_LENGTH];
	sha256_str(Sha256.value(), aSha, sizeof(aSha));
	str_format(aMapFilename, sizeof(aMapFilename), "downloadedmaps/%s_%s.map", m_Info.m_Header.m_aMapName, aSha);

	// save map
	IOHANDLE MapFile = pStorage->OpenFile(aMapFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
	if(!MapFile)
	{
		free(pMapData);
		return false;
	}

	io_write(MapFile, pMapData, m_MapInfo.m_Size);
	io_close(MapFile);

	// free data
	free(pMapData);
	return true;
}

int64_t CDemoPlayer::Time()
{
#if defined(CONF_VIDEORECORDER)
	if(m_UseVideo && IVideo::Current())
	{
		if(!m_WasRecording)
		{
			m_WasRecording = true;
			m_Info.m_LastUpdate = IVideo::Current()->Time();
		}
		return IVideo::Current()->Time();
	}
	else
	{
		const int64_t Now = time_get();
		if(m_WasRecording)
		{
			m_WasRecording = false;
			m_Info.m_LastUpdate = Now;
		}
		return Now;
	}
#else
	return time_get();
#endif
}

void CDemoPlayer::Play()
{
	// Fill in previous and next tick
	while(m_Info.m_PreviousTick == -1)
	{
		DoTick();
		if(!IsPlaying())
		{
			// Empty demo or error playing tick
			return;
		}
	}

	// Initialize playback time. Using `set_new_tick` is essential so that `Time`
	// returns the updated time, otherwise the delta between `m_LastUpdate` and
	// the value that `Time` returns when called in the `Update` function can be
	// very large depending on the time required to load the demo, which causes
	// demo playback to start later. This ensures it always starts at 00:00.
	set_new_tick();
	m_Info.m_CurrentTime = m_Info.m_PreviousTick * time_freq() / SERVER_TICK_SPEED;
	m_Info.m_LastUpdate = Time();
	if(m_Info.m_LiveStateUpdating && m_Info.m_LastScan <= 0)
	{
		m_Info.m_LastScan = m_Info.m_LastUpdate;
	}
}

bool CDemoPlayer::SeekPercent(float Percent)
{
	int WantedTick = m_Info.m_Info.m_FirstTick + round_truncate((m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick) * Percent);
	return SetPos(WantedTick);
}

bool CDemoPlayer::SeekTime(float Seconds)
{
	int WantedTick = m_Info.m_Info.m_CurrentTick + round_truncate(Seconds * (float)SERVER_TICK_SPEED);
	return SetPos(WantedTick);
}

bool CDemoPlayer::SeekTick(ETickOffset TickOffset)
{
	int WantedTick;
	switch(TickOffset)
	{
	case TICK_CURRENT:
		// TODO: https://github.com/ddnet/ddnet/issues/11681
		WantedTick = m_Info.m_Info.m_CurrentTick;
		break;
	case TICK_PREVIOUS:
		WantedTick = m_Info.m_PreviousTick;
		break;
	case TICK_NEXT:
		WantedTick = m_Info.m_NextTick;
		break;
	default:
		dbg_assert_failed("Invalid TickOffset");
	}

	// +1 because SetPos will seek until the given tick is the next tick that
	// will be played back, whereas we want the wanted tick to be played now.
	return SetPos(WantedTick + 1);
}

bool CDemoPlayer::SetPos(int WantedTick)
{
	if(!m_File)
		return false;

	// TODO: Early exit when WantedTick > m_Info.m_Info.m_CurrentTick && WantedTick <= m_Info.m_NextTick with https://github.com/ddnet/ddnet/issues/11681

	int LastSeekableTick = m_Info.m_Info.m_LastTick;
	if(m_Info.m_Info.m_LiveDemo)
	{
		// Make sure we don't seek all the way until the end in a live demo because the chunk data may not be fully written.
		LastSeekableTick -= 2 * SERVER_TICK_SPEED;
	}
	if(LastSeekableTick < m_Info.m_Info.m_FirstTick)
	{
		WantedTick = m_Info.m_Info.m_FirstTick;
	}
	else
	{
		WantedTick = std::clamp(WantedTick, m_Info.m_Info.m_FirstTick, LastSeekableTick);
	}

	// Just the next tick
	if(WantedTick == m_Info.m_NextTick + 1)
	{
		DoTick();
		Play();
		return true;
	}

	const int KeyFrameWantedTick = WantedTick - 5; // -5 because we have to have a current tick and previous tick when we do the playback
	const float Percent = (KeyFrameWantedTick - m_Info.m_Info.m_FirstTick) / (float)(m_Info.m_Info.m_LastTick - m_Info.m_Info.m_FirstTick);

	// get correct key frame
	size_t KeyFrame = std::clamp<size_t>(m_vKeyFrames.size() * Percent, 0, m_vKeyFrames.size() - 1);
	while(KeyFrame < m_vKeyFrames.size() - 1 && m_vKeyFrames[KeyFrame].m_Tick < KeyFrameWantedTick)
		KeyFrame++;
	while(KeyFrame > 0 && m_vKeyFrames[KeyFrame].m_Tick > KeyFrameWantedTick)
		KeyFrame--;

	// TODO Remove `WantedTick <= m_Info.m_NextTick` with https://github.com/ddnet/ddnet/issues/11681
	if(WantedTick <= m_Info.m_Info.m_CurrentTick || // if we are seeking backwards (must be <= for high bandwidth demos) OR
		WantedTick <= m_Info.m_NextTick || // if seeking to current tick OR
		m_Info.m_Info.m_CurrentTick < m_vKeyFrames[KeyFrame].m_Tick || // we are before the wanted KeyFrame OR
		(KeyFrame != m_vKeyFrames.size() - 1 && m_Info.m_Info.m_CurrentTick >= m_vKeyFrames[KeyFrame + 1].m_Tick)) // we are after the wanted KeyFrame
	{
		if(io_seek(m_File, m_vKeyFrames[KeyFrame].m_Filepos, EIoSeekOrigin::START) != 0)
		{
			Stop("Error seeking keyframe position");
			return false;
		}
		m_Info.m_NextTick = -1;
		m_Info.m_Info.m_CurrentTick = -1;
		m_Info.m_PreviousTick = -1;
	}

	// playback everything until we hit our tick
	while(m_Info.m_NextTick < WantedTick)
	{
		DoTick();
		if(!IsPlaying())
		{
			return false;
		}
	}

	Play();

	return true;
}

void CDemoPlayer::SetSpeed(float Speed)
{
	m_Info.m_Info.m_Speed = std::clamp(Speed, 0.f, 256.f);
}

void CDemoPlayer::SetSpeedIndex(int SpeedIndex)
{
	dbg_assert(SpeedIndex >= 0 && SpeedIndex < (int)std::size(DEMO_SPEEDS), "invalid SpeedIndex");
	m_SpeedIndex = SpeedIndex;
	SetSpeed(DEMO_SPEEDS[m_SpeedIndex]);
}

void CDemoPlayer::AdjustSpeedIndex(int Offset)
{
	SetSpeedIndex(std::clamp(m_SpeedIndex + Offset, 0, (int)(std::size(DEMO_SPEEDS) - 1)));
}

void CDemoPlayer::Update(bool RealTime)
{
	const int64_t Now = Time();
	const int64_t Freq = time_freq();
	const int64_t DeltaTime = Now - m_Info.m_LastUpdate;
	m_Info.m_LastUpdate = Now;

	if(m_Info.m_LiveStateUpdating)
	{
		// Determine if demo is live and still being written to, by scanning
		// file again and checking if more ticks are available than before.
		if(Now - m_Info.m_LastScan > Freq)
		{
			const int PreviousLastTick = m_Info.m_Info.m_LastTick;
			const EScanFileResult ScanResult = ScanFile();
			if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
			{
				Stop("Unrecoverable error on incrementally scanning demo file to determine live state");
				return;
			}
			else if(ScanResult == EScanFileResult::SUCCESS)
			{
				// Live state is known when ScanFile succeeded.
				m_Info.m_LiveStateUpdating = false;
			}
			else
			{
				m_Info.m_LiveStateFailedCount++;
				if(m_Info.m_LiveStateFailedCount >= 15)
				{
					// ScanFile keeps failing, which should be unlikely, so this is probably
					// not a live demo but a regular demo that is truncated at the end.
					m_Info.m_LiveStateUpdating = false;
				}
			}
			// Check if we got more ticks also when ScanFile failed, because
			// it could still have found more ticks.
			if(m_Info.m_Info.m_LastTick > PreviousLastTick)
			{
				m_Info.m_Info.m_LiveDemo = true;
				m_Info.m_LiveStateUpdating = false;
				m_Info.m_LiveStateUnchangedCount = 0;
			}
			m_Info.m_LastScan = Now;
			// Try again later if ScanFile failed and no more ticks were found.
		}
	}
	else if(m_Info.m_Info.m_LiveDemo)
	{
		// Scan live demo at tick frequency to smoothly update total time.
		if(Now - m_Info.m_LastScan > Freq / SERVER_TICK_SPEED)
		{
			const int PreviousLastTick = m_Info.m_Info.m_LastTick;
			const EScanFileResult ScanResult = ScanFile();
			if(ScanResult == EScanFileResult::ERROR_UNRECOVERABLE)
			{
				Stop("Unrecoverable error on incrementally scanning live demo file");
				return;
			}
			else if(ScanResult == EScanFileResult::SUCCESS &&
				m_Info.m_Info.m_LastTick == PreviousLastTick)
			{
				m_Info.m_LiveStateUnchangedCount++;
				if(m_Info.m_LiveStateUnchangedCount >= 2 * SERVER_TICK_SPEED)
				{
					// Assume demo stopped being live if we scanned the demo
					// successfully for 2 seconds without reading new ticks.
					m_Info.m_Info.m_LiveDemo = false;
				}
			}
			else
			{
				m_Info.m_LiveStateUnchangedCount = 0;
			}
			m_Info.m_LastScan = Now;
		}
	}

	if(!IsPlaying())
	{
		return;
	}

	if(!m_Info.m_Info.m_Paused)
	{
		if(m_Info.m_Info.m_LiveDemo &&
			m_Info.m_Info.m_Speed > 1.0f &&
			m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick <= (DeltaTime * (double)m_Info.m_Info.m_Speed / Freq + 2) * (float)SERVER_TICK_SPEED)
		{
			// Reset to default speed if we are fast-forwarding to the end of a live demo,
			// to prevent playback error due to final demo chunk data still being written.
			SetSpeedIndex(DEMO_SPEED_INDEX_DEFAULT);
		}

		m_Info.m_CurrentTime += (int64_t)(DeltaTime * (double)m_Info.m_Info.m_Speed);

		// Do more ticks until we reach the current time.
		while(!m_Info.m_Info.m_Paused)
		{
			const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
			if(RealTime && CurrentTickStart > m_Info.m_CurrentTime)
			{
				break;
			}
			DoTick();
			if(!IsPlaying())
			{
				return;
			}
		}
	}

	UpdateTimes();
}

void CDemoPlayer::UpdateTimes()
{
	const int64_t Freq = time_freq();
	const int64_t CurrentTickStart = m_Info.m_Info.m_CurrentTick * Freq / SERVER_TICK_SPEED;
	const int64_t PreviousTickStart = m_Info.m_PreviousTick * Freq / SERVER_TICK_SPEED;
	m_Info.m_IntraTick = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(CurrentTickStart - PreviousTickStart);
	m_Info.m_IntraTickSincePrev = (m_Info.m_CurrentTime - PreviousTickStart) / (float)(Freq / SERVER_TICK_SPEED);
	m_Info.m_TickTime = (m_Info.m_CurrentTime - PreviousTickStart) / (float)Freq;
	m_Info.m_Info.m_LivePlayback = m_Info.m_Info.m_LastTick - m_Info.m_Info.m_CurrentTick < 3 * SERVER_TICK_SPEED;

	if(m_UpdateIntraTimesFunc)
	{
		m_UpdateIntraTimesFunc();
	}
}

void CDemoPlayer::Stop(const char *pErrorMessage)
{
#if defined(CONF_VIDEORECORDER)
	if(m_UseVideo && IVideo::Current())
		IVideo::Current()->Stop();
	m_WasRecording = false;
#endif

	if(!m_File)
		return;

	if(m_pConsole)
	{
		char aBuf[256];
		if(pErrorMessage[0] == '\0')
			str_copy(aBuf, "Stopped playback");
		else
			str_format(aBuf, sizeof(aBuf), "Stopped playback due to error: %s", pErrorMessage);
		m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_player", aBuf);
	}

	io_close(m_File);
	m_File = nullptr;
	m_vKeyFrames.clear();
	str_copy(m_aFilename, "");
	str_copy(m_aErrorMessage, pErrorMessage);
}

void CDemoPlayer::GetDemoName(char *pBuffer, size_t BufferSize) const
{
	fs_split_file_extension(fs_filename(m_aFilename), pBuffer, BufferSize);
}

bool CDemoPlayer::GetDemoInfo(IStorage *pStorage, IConsole *pConsole, const char *pFilename, int StorageType, CDemoHeader *pDemoHeader, CTimelineMarkers *pTimelineMarkers, CMapInfo *pMapInfo, IOHANDLE *pFile, char *pErrorMessage, size_t ErrorMessageSize) const
{
	mem_zero(pDemoHeader, sizeof(CDemoHeader));
	mem_zero(pTimelineMarkers, sizeof(CTimelineMarkers));
	pMapInfo->m_aName[0] = '\0';
	pMapInfo->m_Sha256 = std::nullopt;
	pMapInfo->m_Crc = 0;
	pMapInfo->m_Size = 0;

	IOHANDLE File = pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType);
	if(!File)
	{
		if(pErrorMessage != nullptr)
			str_copy(pErrorMessage, "Could not open demo file", ErrorMessageSize);
		return false;
	}

	if(io_read(File, pDemoHeader, sizeof(CDemoHeader)) != sizeof(CDemoHeader) || !pDemoHeader->Valid())
	{
		if(pErrorMessage != nullptr)
			str_copy(pErrorMessage, "Error reading demo header", ErrorMessageSize);
		mem_zero(pDemoHeader, sizeof(CDemoHeader));
		io_close(File);
		return false;
	}

	if(pDemoHeader->m_Version < gs_OldVersion)
	{
		if(pErrorMessage != nullptr)
			str_format(pErrorMessage, ErrorMessageSize, "Demo version '%d' is not supported", pDemoHeader->m_Version);
		mem_zero(pDemoHeader, sizeof(CDemoHeader));
		io_close(File);
		return false;
	}
	else if(pDemoHeader->m_Version > gs_OldVersion)
	{
		if(io_read(File, pTimelineMarkers, sizeof(CTimelineMarkers)) != sizeof(CTimelineMarkers))
		{
			if(pErrorMessage != nullptr)
				str_copy(pErrorMessage, "Error reading timeline markers", ErrorMessageSize);
			mem_zero(pDemoHeader, sizeof(CDemoHeader));
			io_close(File);
			return false;
		}
	}

	std::optional<SHA256_DIGEST> Sha256;
	if(pDemoHeader->m_Version >= gs_Sha256Version)
	{
		CUuid ExtensionUuid = {};
		const unsigned ExtensionUuidSize = io_read(File, &ExtensionUuid.m_aData, sizeof(ExtensionUuid.m_aData));
		if(ExtensionUuidSize == sizeof(ExtensionUuid.m_aData) && ExtensionUuid == SHA256_EXTENSION)
		{
			SHA256_DIGEST ReadSha256;
			if(io_read(File, &ReadSha256, sizeof(SHA256_DIGEST)) != sizeof(SHA256_DIGEST))
			{
				if(pErrorMessage != nullptr)
					str_copy(pErrorMessage, "Error reading SHA256", ErrorMessageSize);
				mem_zero(pDemoHeader, sizeof(CDemoHeader));
				mem_zero(pTimelineMarkers, sizeof(CTimelineMarkers));
				io_close(File);
				return false;
			}
			Sha256 = ReadSha256;
		}
		else
		{
			// This hopes whatever happened during the version increment didn't add something here
			if(pConsole)
			{
				pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "demo_player", "Demo version incremented, but not by DDNet");
			}
			if(io_seek(File, -(int64_t)ExtensionUuidSize, EIoSeekOrigin::CURRENT) != 0)
			{
				if(pErrorMessage != nullptr)
					str_copy(pErrorMessage, "Error rewinding SHA256 extension UUID", ErrorMessageSize);
				mem_zero(pDemoHeader, sizeof(CDemoHeader));
				mem_zero(pTimelineMarkers, sizeof(CTimelineMarkers));
				io_close(File);
				return false;
			}
		}
	}

	str_copy(pMapInfo->m_aName, pDemoHeader->m_aMapName);
	pMapInfo->m_Sha256 = Sha256;
	pMapInfo->m_Crc = bytes_be_to_uint(pDemoHeader->m_aMapCrc);
	pMapInfo->m_Size = bytes_be_to_uint(pDemoHeader->m_aMapSize);

	if(pFile == nullptr)
		io_close(File);
	else
		*pFile = File;

	return true;
}

class CDemoRecordingListener : public CDemoPlayer::IListener
{
public:
	CDemoRecorder *m_pDemoRecorder;
	CDemoPlayer *m_pDemoPlayer;
	bool m_Stop;
	int m_StartTick;
	int m_EndTick;

	void OnDemoPlayerSnapshot(void *pData, int Size) override
	{
		const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();

		if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
			m_Stop = true;
		else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
			m_pDemoRecorder->RecordSnapshot(pInfo->m_Info.m_CurrentTick, pData, Size);
	}

	void OnDemoPlayerMessage(void *pData, int Size) override
	{
		const CDemoPlayer::CPlaybackInfo *pInfo = m_pDemoPlayer->Info();

		if(m_EndTick != -1 && pInfo->m_Info.m_CurrentTick > m_EndTick)
			m_Stop = true;
		else if(m_StartTick == -1 || pInfo->m_Info.m_CurrentTick >= m_StartTick)
			m_pDemoRecorder->RecordMessage(pData, Size);
	}
};

void CDemoEditor::Init(CSnapshotDelta *pSnapshotDelta, CSnapshotDelta *pSnapshotDeltaSixup, IConsole *pConsole, IStorage *pStorage)
{
	m_pSnapshotDelta = pSnapshotDelta;
	m_pSnapshotDeltaSixup = pSnapshotDeltaSixup;
	m_pConsole = pConsole;
	m_pStorage = pStorage;
}

bool CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser)
{
	CDemoPlayer DemoPlayer(m_pSnapshotDelta, m_pSnapshotDeltaSixup, false);
	if(DemoPlayer.Load(m_pStorage, m_pConsole, pDemo, IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
		return false;

	const CMapInfo *pMapInfo = DemoPlayer.GetMapInfo();
	const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info();

	std::optional<SHA256_DIGEST> Sha256 = pMapInfo->m_Sha256;
	if(pInfo->m_Header.m_Version < gs_Sha256Version)
	{
		if(DemoPlayer.ExtractMap(m_pStorage))
		{
			Sha256 = pMapInfo->m_Sha256;
		}
	}
	if(!Sha256.has_value())
	{
		log_error_color(DEMO_PRINT_COLOR, "demo/slice", "Failed to start demo slicing because map SHA256 could not be determined.");
		return false;
	}

	CDemoRecorder DemoRecorder(m_pSnapshotDelta);
	unsigned char *pMapData = DemoPlayer.GetMapData(m_pStorage);
	const int Result = DemoRecorder.Start(m_pStorage, m_pConsole, pDst, pInfo->m_Header.m_aNetversion, pMapInfo->m_aName, Sha256.value(), pMapInfo->m_Crc, pInfo->m_Header.m_aType, pMapInfo->m_Size, pMapData, nullptr, pfnFilter, pUser) == -1;
	free(pMapData);
	if(Result != 0)
	{
		DemoPlayer.Stop();
		return false;
	}

	CDemoRecordingListener Listener;
	Listener.m_pDemoRecorder = &DemoRecorder;
	Listener.m_pDemoPlayer = &DemoPlayer;
	Listener.m_Stop = false;
	Listener.m_StartTick = StartTick;
	Listener.m_EndTick = EndTick;
	DemoPlayer.SetListener(&Listener);

	DemoPlayer.Play();

	while(DemoPlayer.IsPlaying() && !Listener.m_Stop)
	{
		DemoPlayer.Update(false);

		if(pInfo->m_Info.m_Paused)
			break;
	}

	// Copy timeline markers to sliced demo
	for(int i = 0; i < pInfo->m_Info.m_NumTimelineMarkers; i++)
	{
		if((StartTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] >= StartTick) && (EndTick == -1 || pInfo->m_Info.m_aTimelineMarkers[i] <= EndTick))
		{
			DemoRecorder.AddDemoMarker(pInfo->m_Info.m_aTimelineMarkers[i]);
		}
	}

	DemoPlayer.Stop();
	DemoRecorder.Stop(IDemoRecorder::EStopMode::KEEP_FILE);
	return true;
}
