diff --git a/src/base/math.h b/src/base/math.h index edbee831d81..1a6692ea076 100644 --- a/src/base/math.h +++ b/src/base/math.h @@ -27,6 +27,20 @@ constexpr inline T mix(const T a, const T b, TB amount) return a + (b - a) * amount; } +template +inline T bezier(const T p0, const T p1, const T p2, const T p3, TB amount) +{ + // De-Casteljau Algorithm + const T c10 = mix(p0, p1, amount); + const T c11 = mix(p1, p2, amount); + const T c12 = mix(p2, p3, amount); + + const T c20 = mix(c10, c11, amount); + const T c21 = mix(c11, c12, amount); + + return mix(c20, c21, amount); // c30 +} + inline float random_float() { return rand() / (float)(RAND_MAX); diff --git a/src/engine/shared/datafile.cpp b/src/engine/shared/datafile.cpp index c0c08c5dc95..86d6ea8ae71 100644 --- a/src/engine/shared/datafile.cpp +++ b/src/engine/shared/datafile.cpp @@ -87,6 +87,7 @@ struct CDatafile CDatafileHeader m_Header; int m_DataStartOffset; char **m_ppDataPtrs; + int *m_pDataSizes; char *m_pData; }; @@ -163,18 +164,27 @@ bool CDataFileReader::Open(class IStorage *pStorage, const char *pFilename, int unsigned AllocSize = Size; AllocSize += sizeof(CDatafile); // add space for info structure AllocSize += Header.m_NumRawData * sizeof(void *); // add space for data pointers + AllocSize += Header.m_NumRawData * sizeof(int); // add space for data sizes + if(Size > (((int64_t)1) << 31) || Header.m_NumItemTypes < 0 || Header.m_NumItems < 0 || Header.m_NumRawData < 0 || Header.m_ItemSize < 0) + { + io_close(File); + dbg_msg("datafile", "unable to load file, invalid file information"); + return false; + } CDatafile *pTmpDataFile = (CDatafile *)malloc(AllocSize); pTmpDataFile->m_Header = Header; pTmpDataFile->m_DataStartOffset = sizeof(CDatafileHeader) + Size; pTmpDataFile->m_ppDataPtrs = (char **)(pTmpDataFile + 1); - pTmpDataFile->m_pData = (char *)(pTmpDataFile + 1) + Header.m_NumRawData * sizeof(char *); + pTmpDataFile->m_pDataSizes = (int *)(pTmpDataFile->m_ppDataPtrs + Header.m_NumRawData); + pTmpDataFile->m_pData = (char *)(pTmpDataFile->m_pDataSizes + Header.m_NumRawData); pTmpDataFile->m_File = File; pTmpDataFile->m_Sha256 = Sha256; pTmpDataFile->m_Crc = Crc; - // clear the data pointers + // clear the data pointers and sizes mem_zero(pTmpDataFile->m_ppDataPtrs, Header.m_NumRawData * sizeof(void *)); + mem_zero(pTmpDataFile->m_pDataSizes, Header.m_NumRawData * sizeof(int)); // read types, offsets, sizes and item data unsigned ReadSize = io_read(File, pTmpDataFile->m_pData, Size); @@ -224,7 +234,11 @@ bool CDataFileReader::Close() // free the data that is loaded for(int i = 0; i < m_pDataFile->m_Header.m_NumRawData; i++) + { free(m_pDataFile->m_ppDataPtrs[i]); + m_pDataFile->m_ppDataPtrs[i] = nullptr; + m_pDataFile->m_pDataSizes[i] = 0; + } io_close(m_pDataFile->m_File); free(m_pDataFile); @@ -258,21 +272,30 @@ int CDataFileReader::GetFileDataSize(int Index) const if(Index == m_pDataFile->m_Header.m_NumRawData - 1) return m_pDataFile->m_Header.m_DataSize - m_pDataFile->m_Info.m_pDataOffsets[Index]; + return m_pDataFile->m_Info.m_pDataOffsets[Index + 1] - m_pDataFile->m_Info.m_pDataOffsets[Index]; } // returns the size of the resulting data int CDataFileReader::GetDataSize(int Index) const { - if(!m_pDataFile) + if(!m_pDataFile || Index < 0 || Index >= m_pDataFile->m_Header.m_NumRawData) { return 0; } - if(m_pDataFile->m_Header.m_Version == 4) - return m_pDataFile->m_Info.m_pDataSizes[Index]; - else - return GetFileDataSize(Index); + if(!m_pDataFile->m_ppDataPtrs[Index]) + { + if(m_pDataFile->m_Header.m_Version >= 4) + { + return m_pDataFile->m_Info.m_pDataSizes[Index]; + } + else + { + return GetFileDataSize(Index); + } + } + return m_pDataFile->m_pDataSizes[Index]; } void *CDataFileReader::GetDataImpl(int Index, int Swap) @@ -303,6 +326,7 @@ void *CDataFileReader::GetDataImpl(int Index, int Swap) log_trace("datafile", "loading data index=%d size=%d uncompressed=%lu", Index, DataSize, UncompressedSize); m_pDataFile->m_ppDataPtrs[Index] = (char *)malloc(UncompressedSize); + m_pDataFile->m_pDataSizes[Index] = UncompressedSize; // read the compressed data io_seek(m_pDataFile->m_File, m_pDataFile->m_DataStartOffset + m_pDataFile->m_Info.m_pDataOffsets[Index], IOSEEK_START); @@ -323,6 +347,7 @@ void *CDataFileReader::GetDataImpl(int Index, int Swap) // load the data log_trace("datafile", "loading data index=%d size=%d", Index, DataSize); m_pDataFile->m_ppDataPtrs[Index] = (char *)malloc(DataSize); + m_pDataFile->m_pDataSizes[Index] = DataSize; io_seek(m_pDataFile->m_File, m_pDataFile->m_DataStartOffset + m_pDataFile->m_Info.m_pDataOffsets[Index], IOSEEK_START); io_read(m_pDataFile->m_File, m_pDataFile->m_ppDataPtrs[Index], DataSize); } @@ -346,14 +371,24 @@ void *CDataFileReader::GetDataSwapped(int Index) return GetDataImpl(Index, 1); } +void CDataFileReader::ReplaceData(int Index, char *pData, size_t Size) +{ + // make sure the data has been loaded + GetDataImpl(Index, 0); + + UnloadData(Index); + m_pDataFile->m_ppDataPtrs[Index] = pData; + m_pDataFile->m_pDataSizes[Index] = Size; +} + void CDataFileReader::UnloadData(int Index) { if(Index < 0 || Index >= m_pDataFile->m_Header.m_NumRawData) return; - // free(m_pDataFile->m_ppDataPtrs[Index]); m_pDataFile->m_ppDataPtrs[Index] = nullptr; + m_pDataFile->m_pDataSizes[Index] = 0; } int CDataFileReader::GetItemSize(int Index) const diff --git a/src/engine/shared/datafile.h b/src/engine/shared/datafile.h index c55f1fccfc7..02c1a649594 100644 --- a/src/engine/shared/datafile.h +++ b/src/engine/shared/datafile.h @@ -38,6 +38,7 @@ class CDataFileReader void *GetData(int Index); void *GetDataSwapped(int Index); // makes sure that the data is 32bit LE ints when saved int GetDataSize(int Index) const; + void ReplaceData(int Index, char *pData, size_t Size); void UnloadData(int Index); int NumData() const; diff --git a/src/engine/shared/map.cpp b/src/engine/shared/map.cpp index 56fbde15305..26d95dcfa2e 100644 --- a/src/engine/shared/map.cpp +++ b/src/engine/shared/map.cpp @@ -1,8 +1,11 @@ /* (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 "map.h" + #include +#include + CMap::CMap() = default; void *CMap::GetData(int Index) @@ -60,7 +63,38 @@ bool CMap::Load(const char *pMapName) IStorage *pStorage = Kernel()->RequestInterface(); if(!pStorage) return false; - return m_DataFile.Open(pStorage, pMapName, IStorage::TYPE_ALL); + if(!m_DataFile.Open(pStorage, pMapName, IStorage::TYPE_ALL)) + return false; + // check version + const CMapItemVersion *pItem = (CMapItemVersion *)m_DataFile.FindItem(MAPITEMTYPE_VERSION, 0); + if(!pItem || pItem->m_Version != CMapItemVersion::CURRENT_VERSION) + return false; + + // replace compressed tile layers with uncompressed ones + int GroupsStart, GroupsNum, LayersStart, LayersNum; + m_DataFile.GetType(MAPITEMTYPE_GROUP, &GroupsStart, &GroupsNum); + m_DataFile.GetType(MAPITEMTYPE_LAYER, &LayersStart, &LayersNum); + for(int g = 0; g < GroupsNum; g++) + { + const CMapItemGroup *pGroup = static_cast(m_DataFile.GetItem(GroupsStart + g)); + for(int l = 0; l < pGroup->m_NumLayers; l++) + { + CMapItemLayer *pLayer = static_cast(m_DataFile.GetItem(LayersStart + pGroup->m_StartLayer + l)); + if(pLayer->m_Type == LAYERTYPE_TILES) + { + CMapItemLayerTilemap *pTilemap = reinterpret_cast(pLayer); + if(pTilemap->m_Version >= CMapItemLayerTilemap::TILE_SKIP_MIN_VERSION) + { + const size_t TilemapSize = (size_t)pTilemap->m_Width * pTilemap->m_Height * sizeof(CTile); + CTile *pTiles = static_cast(malloc(TilemapSize)); + ExtractTiles(pTiles, (size_t)pTilemap->m_Width * pTilemap->m_Height, static_cast(m_DataFile.GetData(pTilemap->m_Data)), m_DataFile.GetDataSize(pTilemap->m_Data) / sizeof(CTile)); + m_DataFile.ReplaceData(pTilemap->m_Data, reinterpret_cast(pTiles), TilemapSize); + } + } + } + } + + return true; } void CMap::Unload() @@ -93,4 +127,20 @@ int CMap::MapSize() const return m_DataFile.MapSize(); } +void CMap::ExtractTiles(CTile *pDest, size_t DestSize, const CTile *pSrc, size_t SrcSize) +{ + size_t DestIndex = 0; + size_t SrcIndex = 0; + while(DestIndex < DestSize && SrcIndex < SrcSize) + { + for(unsigned Counter = 0; Counter <= pSrc[SrcIndex].m_Skip && DestIndex < DestSize; Counter++) + { + pDest[DestIndex] = pSrc[SrcIndex]; + pDest[DestIndex].m_Skip = 0; + DestIndex++; + } + SrcIndex++; + } +} + extern IEngineMap *CreateEngineMap() { return new CMap; } diff --git a/src/engine/shared/map.h b/src/engine/shared/map.h index 086567310dc..b5395f08b85 100644 --- a/src/engine/shared/map.h +++ b/src/engine/shared/map.h @@ -15,6 +15,8 @@ class CMap : public IEngineMap public: CMap(); + CDataFileReader *GetReader() { return &m_DataFile; } + void *GetData(int Index) override; int GetDataSize(int Index) const override; void *GetDataSwapped(int Index) override; @@ -35,6 +37,8 @@ class CMap : public IEngineMap SHA256_DIGEST Sha256() const override; unsigned Crc() const override; int MapSize() const override; + + static void ExtractTiles(class CTile *pDest, size_t DestSize, const class CTile *pSrc, size_t SrcSize); }; #endif diff --git a/src/game/client/components/mapimages.cpp b/src/game/client/components/mapimages.cpp index bf9bbc05e0f..6dbbe9e4b45 100644 --- a/src/game/client/components/mapimages.cpp +++ b/src/game/client/components/mapimages.cpp @@ -105,13 +105,15 @@ void CMapImages::OnMapLoadImpl(class CLayers *pLayers, IMap *pMap) for(int i = 0; i < m_Count; i++) { int LoadFlag = (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 1) != 0) ? TextureLoadFlag : 0) | (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 2) != 0) ? 0 : (Graphics()->IsTileBufferingEnabled() ? IGraphics::TEXLOAD_NO_2D_TEXTURE : 0)); - CMapItemImage *pImg = (CMapItemImage *)pMap->GetItem(Start + i); - if(pImg->m_External) + const CMapItemImage_v2 *pImg = (CMapItemImage_v2 *)pMap->GetItem(Start + i); + const int Format = pImg->m_Version < CMapItemImage_v2::CURRENT_VERSION ? CImageInfo::FORMAT_RGBA : pImg->m_Format; + if(pImg->m_External || (Format != CImageInfo::FORMAT_RGB && Format != CImageInfo::FORMAT_RGBA)) { char aPath[IO_MAX_PATH_LENGTH]; char *pName = (char *)pMap->GetData(pImg->m_ImageName); str_format(aPath, sizeof(aPath), "mapres/%s.png", pName); m_aTextures[i] = Graphics()->LoadTexture(aPath, IStorage::TYPE_ALL, CImageInfo::FORMAT_AUTO, LoadFlag); + pMap->UnloadData(pImg->m_ImageName); } else { @@ -119,7 +121,8 @@ void CMapImages::OnMapLoadImpl(class CLayers *pLayers, IMap *pMap) char *pName = (char *)pMap->GetData(pImg->m_ImageName); char aTexName[128]; str_format(aTexName, sizeof(aTexName), "%s %s", "embedded:", pName); - m_aTextures[i] = Graphics()->LoadTextureRaw(pImg->m_Width, pImg->m_Height, CImageInfo::FORMAT_RGBA, pData, CImageInfo::FORMAT_RGBA, LoadFlag, aTexName); + m_aTextures[i] = Graphics()->LoadTextureRaw(pImg->m_Width, pImg->m_Height, Format, pData, CImageInfo::FORMAT_RGBA, LoadFlag, aTexName); + pMap->UnloadData(pImg->m_ImageName); pMap->UnloadData(pImg->m_ImageData); } } diff --git a/src/game/client/components/maplayers.cpp b/src/game/client/components/maplayers.cpp index 043f96c8945..27519dcb06d 100644 --- a/src/game/client/components/maplayers.cpp +++ b/src/game/client/components/maplayers.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -60,22 +61,15 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels CMapLayers *pThis = (CMapLayers *)pUser; Channels = ColorRGBA(); - CEnvPoint *pPoints = 0; + const CMapBasedEnvelopePointAccess EnvelopePoints(pThis->m_pLayers->Map()); - { - int Start, Num; - pThis->m_pLayers->Map()->GetType(MAPITEMTYPE_ENVPOINTS, &Start, &Num); - if(Num) - pPoints = (CEnvPoint *)pThis->m_pLayers->Map()->GetItem(Start); - } - - int Start, Num; - pThis->m_pLayers->Map()->GetType(MAPITEMTYPE_ENVELOPE, &Start, &Num); + int EnvStart, EnvNum; + pThis->m_pLayers->Map()->GetType(MAPITEMTYPE_ENVELOPE, &EnvStart, &EnvNum); - if(Env >= Num) + if(EnvelopePoints.NumPoints() == 0 || Env < 0 || Env >= EnvNum) return; - CMapItemEnvelope *pItem = (CMapItemEnvelope *)pThis->m_pLayers->Map()->GetItem(Start + Env); + const CMapItemEnvelope *pItem = (CMapItemEnvelope *)pThis->m_pLayers->Map()->GetItem(EnvStart + Env); const auto TickToNanoSeconds = std::chrono::nanoseconds(1s) / (int64_t)pThis->Client()->GameTickSpeed(); @@ -117,7 +111,7 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels MinTick * TickToNanoSeconds; } } - CRenderTools::RenderEvalEnvelope(pPoints + pItem->m_StartPoint, pItem->m_NumPoints, 4, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Channels); + CRenderTools::RenderEvalEnvelope(&EnvelopePoints, 4, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Channels); } else { @@ -142,7 +136,7 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels s_Time += CurTime - s_LastLocalTime; s_LastLocalTime = CurTime; } - CRenderTools::RenderEvalEnvelope(pPoints + pItem->m_StartPoint, pItem->m_NumPoints, 4, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Channels); + CRenderTools::RenderEvalEnvelope(&EnvelopePoints, 4, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Channels); } } diff --git a/src/game/client/render.h b/src/game/client/render.h index 1ac873dcd8f..6af7607bebe 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -20,6 +20,8 @@ struct CDataSprite; } struct CDataSprite; struct CEnvPoint; +struct CEnvPointBezier; +struct CEnvPointBezier_upstream; struct CMapItemGroup; struct CMapItemGroupEx; struct CQuad; @@ -71,6 +73,29 @@ enum TILERENDERFLAG_EXTEND = 4, }; +class IEnvelopePointAccess +{ +public: + virtual int NumPoints() const = 0; + virtual const CEnvPoint *GetPoint(int Index) const = 0; + virtual const CEnvPointBezier *GetBezier(int Index) const = 0; +}; + +class CMapBasedEnvelopePointAccess : public IEnvelopePointAccess +{ + int m_NumPoints; + CEnvPoint *m_pPoints; + CEnvPointBezier *m_pPointsBezier; + CEnvPointBezier_upstream *m_pPointsBezierUpstream; + +public: + CMapBasedEnvelopePointAccess(class CDataFileReader *pReader); + CMapBasedEnvelopePointAccess(class IMap *pMap); + int NumPoints() const override; + const CEnvPoint *GetPoint(int Index) const override; + const CEnvPointBezier *GetBezier(int Index) const override; +}; + typedef void (*ENVELOPE_EVAL)(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser); class CRenderTools @@ -117,7 +142,7 @@ class CRenderTools void RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f); // map render methods (render_map.cpp) - static void RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result); + static void RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result); void RenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser); void ForceRenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser, float Alpha = 1.0f); void RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset); diff --git a/src/game/client/render_map.cpp b/src/game/client/render_map.cpp index 4c7c43349b5..92defd4abaf 100644 --- a/src/game/client/render_map.cpp +++ b/src/game/client/render_map.cpp @@ -3,23 +3,225 @@ #include #include +#include #include #include +#include +#include #include "render.h" #include #include +#include #include #include using namespace std::chrono_literals; -void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result) +CMapBasedEnvelopePointAccess::CMapBasedEnvelopePointAccess(CDataFileReader *pReader) { + bool FoundBezierEnvelope = false; + int EnvStart, EnvNum; + pReader->GetType(MAPITEMTYPE_ENVELOPE, &EnvStart, &EnvNum); + for(int EnvIndex = 0; EnvIndex < EnvNum; EnvIndex++) + { + CMapItemEnvelope *pEnvelope = static_cast(pReader->GetItem(EnvStart + EnvIndex)); + if(pEnvelope->m_Version >= CMapItemEnvelope_v3::CURRENT_VERSION) + { + FoundBezierEnvelope = true; + break; + } + } + + if(FoundBezierEnvelope) + { + m_pPoints = nullptr; + m_pPointsBezier = nullptr; + + int EnvPointStart, FakeEnvPointNum; + pReader->GetType(MAPITEMTYPE_ENVPOINTS, &EnvPointStart, &FakeEnvPointNum); + if(FakeEnvPointNum > 0) + m_pPointsBezierUpstream = static_cast(pReader->GetItem(EnvPointStart)); + else + m_pPointsBezierUpstream = nullptr; + + m_NumPoints = pReader->GetItemSize(EnvPointStart) / sizeof(CEnvPointBezier_upstream); + } + else + { + int EnvPointStart, FakeEnvPointNum; + pReader->GetType(MAPITEMTYPE_ENVPOINTS, &EnvPointStart, &FakeEnvPointNum); + if(FakeEnvPointNum > 0) + m_pPoints = static_cast(pReader->GetItem(EnvPointStart)); + else + m_pPoints = nullptr; + + m_NumPoints = pReader->GetItemSize(EnvPointStart) / sizeof(CEnvPoint); + + int EnvPointBezierStart, FakeEnvPointBezierNum; + pReader->GetType(MAPITEMTYPE_ENVPOINTS_BEZIER, &EnvPointBezierStart, &FakeEnvPointBezierNum); + const int NumPointsBezier = pReader->GetItemSize(EnvPointBezierStart) / sizeof(CEnvPointBezier); + if(FakeEnvPointBezierNum > 0 && m_NumPoints == NumPointsBezier) + m_pPointsBezier = static_cast(pReader->GetItem(EnvPointBezierStart)); + else + m_pPointsBezier = nullptr; + + m_pPointsBezierUpstream = nullptr; + } +} + +CMapBasedEnvelopePointAccess::CMapBasedEnvelopePointAccess(IMap *pMap) : + CMapBasedEnvelopePointAccess(static_cast(pMap)->GetReader()) +{ +} + +int CMapBasedEnvelopePointAccess::NumPoints() const +{ + return m_NumPoints; +} + +const CEnvPoint *CMapBasedEnvelopePointAccess::GetPoint(int Index) const +{ + if(Index < 0 || Index >= m_NumPoints) + return nullptr; + if(m_pPoints != nullptr) + return &m_pPoints[Index]; + if(m_pPointsBezierUpstream != nullptr) + return &m_pPointsBezierUpstream[Index]; + return nullptr; +} + +const CEnvPointBezier *CMapBasedEnvelopePointAccess::GetBezier(int Index) const +{ + if(Index < 0 || Index >= m_NumPoints) + return nullptr; + if(m_pPointsBezier != nullptr) + return &m_pPointsBezier[Index]; + if(m_pPointsBezierUpstream != nullptr) + return &m_pPointsBezierUpstream[Index].m_Bezier; + return nullptr; +} + +static void ValidateFCurve(const vec2 &p0, vec2 &p1, vec2 &p2, const vec2 &p3) +{ + // validate the bezier curve + p1.x = clamp(p1.x, p0.x, p3.x); + p2.x = clamp(p2.x, p0.x, p3.x); +} + +static double CubicRoot(double x) +{ + if(x == 0.0) + return 0.0; + else if(x < 0.0) + return -std::exp(std::log(-x) / 3.0); + else + return std::exp(std::log(x) / 3.0); +} + +static float SolveBezier(float x, float p0, float p1, float p2, float p3) +{ + // check for valid f-curve + // we only take care of monotonic bezier curves, so there has to be exactly 1 real solution + if(!(p0 <= x && x <= p3) || !(p0 <= p1 && p1 <= p3) || !(p0 <= p2 && p2 <= p3)) + return 0.0f; + + const double x3 = -p0 + 3.0 * p1 - 3.0 * p2 + p3; + const double x2 = 3.0 * p0 - 6.0 * p1 + 3.0 * p2; + const double x1 = -3.0 * p0 + 3.0 * p1; + const double x0 = p0 - x; + + if(x3 == 0.0 && x2 == 0.0) + { + // linear + // a * t + b = 0 + const double a = x1; + const double b = x0; + + if(a == 0.0) + return 0.0f; + return -b / a; + } + else if(x3 == 0.0) + { + // quadratic + // t * t + b * t +c = 0 + const double b = x1 / x2; + const double c = x0 / x2; + + if(c == 0.0) + return 0.0f; + + const double D = b * b - 4.0 * c; + const double SqrtD = std::sqrt(D); + + const double t = (-b + SqrtD) / 2.0; + + if(0.0 <= t && t <= 1.0001f) + return t; + return (-b - SqrtD) / 2.0; + } + else + { + // cubic + // t * t * t + a * t * t + b * t * t + c = 0 + const double a = x2 / x3; + const double b = x1 / x3; + const double c = x0 / x3; + + // substitute t = y - a / 3 + const double sub = a / 3.0; + + // depressed form x^3 + px + q = 0 + // cardano's method + const double p = b / 3.0 - a * a / 9.0; + const double q = (2.0 * a * a * a / 27.0 - a * b / 3.0 + c) / 2.0; + + const double D = q * q + p * p * p; + + if(D > 0.0) + { + // only one 'real' solution + const double s = std::sqrt(D); + return CubicRoot(s - q) - CubicRoot(s + q) - sub; + } + else if(D == 0.0) + { + // one single, one double solution or triple solution + const double s = CubicRoot(-q); + const double t = 2.0 * s - sub; + + if(0.0 <= t && t <= 1.0001f) + return t; + return (-s - sub); + } + else + { + // Casus irreductibilis ... ,_, + const double phi = std::acos(-q / std::sqrt(-(p * p * p))) / 3.0; + const double s = 2.0 * std::sqrt(-p); + + const double t1 = s * std::cos(phi) - sub; + + if(0.0 <= t1 && t1 <= 1.0001f) + return t1; + + const double t2 = -s * std::cos(phi + pi / 3.0) - sub; + + if(0.0 <= t2 && t2 <= 1.0001f) + return t2; + return -s * std::cos(phi - pi / 3.0) - sub; + } + } +} + +void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result) +{ + const int NumPoints = pPoints->NumPoints(); if(NumPoints == 0) { Result = ColorRGBA(); @@ -28,14 +230,16 @@ void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Cha if(NumPoints == 1) { - Result.r = fx2f(pPoints[0].m_aValues[0]); - Result.g = fx2f(pPoints[0].m_aValues[1]); - Result.b = fx2f(pPoints[0].m_aValues[2]); - Result.a = fx2f(pPoints[0].m_aValues[3]); + const CEnvPoint *pFirstPoint = pPoints->GetPoint(0); + Result.r = fx2f(pFirstPoint->m_aValues[0]); + Result.g = fx2f(pFirstPoint->m_aValues[1]); + Result.b = fx2f(pFirstPoint->m_aValues[2]); + Result.a = fx2f(pFirstPoint->m_aValues[3]); return; } - int64_t MaxPointTime = (int64_t)pPoints[NumPoints - 1].m_Time * std::chrono::nanoseconds(1ms).count(); + const CEnvPoint *pLastPoint = pPoints->GetPoint(NumPoints - 1); + const int64_t MaxPointTime = (int64_t)pLastPoint->m_Time * std::chrono::nanoseconds(1ms).count(); if(MaxPointTime > 0) // TODO: remove this check when implementing a IO check for maps(in this case broken envelopes) TimeNanos = std::chrono::nanoseconds(TimeNanos.count() % MaxPointTime); else @@ -44,31 +248,70 @@ void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Cha int TimeMillis = (int)(TimeNanos / std::chrono::nanoseconds(1ms).count()).count(); for(int i = 0; i < NumPoints - 1; i++) { - if(TimeMillis >= pPoints[i].m_Time && TimeMillis <= pPoints[i + 1].m_Time) + const CEnvPoint *pCurrentPoint = pPoints->GetPoint(i); + const CEnvPoint *pNextPoint = pPoints->GetPoint(i + 1); + if(TimeMillis >= pCurrentPoint->m_Time && TimeMillis <= pNextPoint->m_Time) { - float Delta = pPoints[i + 1].m_Time - pPoints[i].m_Time; - float a = (float)(((double)TimeNanos.count() / (double)std::chrono::nanoseconds(1ms).count()) - pPoints[i].m_Time) / Delta; + const float Delta = pNextPoint->m_Time - pCurrentPoint->m_Time; + float a = (float)(((double)TimeNanos.count() / (double)std::chrono::nanoseconds(1ms).count()) - pCurrentPoint->m_Time) / Delta; - if(pPoints[i].m_Curvetype == CURVETYPE_SMOOTH) - a = -2 * a * a * a + 3 * a * a; // second hermite basis - else if(pPoints[i].m_Curvetype == CURVETYPE_SLOW) + switch(pCurrentPoint->m_Curvetype) + { + case CURVETYPE_STEP: + a = 0.0f; + break; + + case CURVETYPE_SLOW: a = a * a * a; - else if(pPoints[i].m_Curvetype == CURVETYPE_FAST) + break; + + case CURVETYPE_FAST: + a = 1.0f - a; + a = 1.0f - a * a * a; + break; + + case CURVETYPE_SMOOTH: + a = -2.0f * a * a * a + 3.0f * a * a; // second hermite basis + break; + + case CURVETYPE_BEZIER: { - a = 1 - a; - a = 1 - a * a * a; + const CEnvPointBezier *pCurrentPointBezier = pPoints->GetBezier(i); + const CEnvPointBezier *pNextPointBezier = pPoints->GetBezier(i + 1); + if(pCurrentPointBezier == nullptr || pNextPointBezier == nullptr) + break; // fallback to linear + for(int c = 0; c < Channels; c++) + { + // monotonic 2d cubic bezier curve + const vec2 p0 = vec2(pCurrentPoint->m_Time / 1000.0f, fx2f(pCurrentPoint->m_aValues[c])); + const vec2 p3 = vec2(pNextPoint->m_Time / 1000.0f, fx2f(pNextPoint->m_aValues[c])); + + const vec2 OutTang = vec2(pCurrentPointBezier->m_aOutTangentDeltaX[c] / 1000.0f, fx2f(pCurrentPointBezier->m_aOutTangentDeltaY[c])); + const vec2 InTang = -vec2(pNextPointBezier->m_aInTangentDeltaX[c] / 1000.0f, fx2f(pNextPointBezier->m_aInTangentDeltaY[c])); + vec2 p1 = p0 + OutTang; + vec2 p2 = p3 - InTang; + + // validate bezier curve + ValidateFCurve(p0, p1, p2, p3); + + // solve x(a) = time for a + a = clamp(SolveBezier(TimeMillis / 1000.0f, p0.x, p1.x, p2.x, p3.x), 0.0f, 1.0f); + + // value = y(t) + Result[c] = bezier(p0.y, p1.y, p2.y, p3.y, a); + } + return; } - else if(pPoints[i].m_Curvetype == CURVETYPE_STEP) - a = 0; - else - { - // linear + + case CURVETYPE_LINEAR: [[fallthrough]]; + default: + break; } for(int c = 0; c < Channels; c++) { - float v0 = fx2f(pPoints[i].m_aValues[c]); - float v1 = fx2f(pPoints[i + 1].m_aValues[c]); + const float v0 = fx2f(pCurrentPoint->m_aValues[c]); + const float v1 = fx2f(pNextPoint->m_aValues[c]); Result[c] = v0 + (v1 - v0) * a; } @@ -76,10 +319,10 @@ void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Cha } } - Result.r = fx2f(pPoints[NumPoints - 1].m_aValues[0]); - Result.g = fx2f(pPoints[NumPoints - 1].m_aValues[1]); - Result.b = fx2f(pPoints[NumPoints - 1].m_aValues[2]); - Result.a = fx2f(pPoints[NumPoints - 1].m_aValues[3]); + Result.r = fx2f(pLastPoint->m_aValues[0]); + Result.g = fx2f(pLastPoint->m_aValues[1]); + Result.b = fx2f(pLastPoint->m_aValues[2]); + Result.a = fx2f(pLastPoint->m_aValues[3]); } static void Rotate(CPoint *pCenter, CPoint *pPoint, float Rotation) diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index fe85dca9333..e3c76587f90 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -2116,19 +2116,26 @@ void CEditor::DoQuadEnvelopes(const std::vector &vQuads, IGraphics::CText continue; //QuadParams - const CPoint *pPoints = vQuads[j].m_aPoints; + const CPoint *pPivotPoint = &vQuads[j].m_aPoints[4]; for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size() - 1; i++) { - float OffsetX = fx2f(apEnvelope[j]->m_vPoints[i].m_aValues[0]); - float OffsetY = fx2f(apEnvelope[j]->m_vPoints[i].m_aValues[1]); - vec2 Pos0 = vec2(fx2f(pPoints[4].x) + OffsetX, fx2f(pPoints[4].y) + OffsetY); + ColorRGBA Result; + apEnvelope[j]->Eval(apEnvelope[j]->m_vPoints[i].m_Time / 1000.0f + 0.000001f, Result); + vec2 Pos0 = vec2(fx2f(pPivotPoint->x) + Result.r, fx2f(pPivotPoint->y) + Result.g); + + const int Steps = 15; + for(int n = 1; n <= Steps; n++) + { + const float Time = mix(apEnvelope[j]->m_vPoints[i].m_Time, apEnvelope[j]->m_vPoints[i + 1].m_Time, (float)n / Steps); + apEnvelope[j]->Eval(Time / 1000.0f - 0.000001f, Result); - OffsetX = fx2f(apEnvelope[j]->m_vPoints[i + 1].m_aValues[0]); - OffsetY = fx2f(apEnvelope[j]->m_vPoints[i + 1].m_aValues[1]); - vec2 Pos1 = vec2(fx2f(pPoints[4].x) + OffsetX, fx2f(pPoints[4].y) + OffsetY); + vec2 Pos1 = vec2(fx2f(pPivotPoint->x) + Result.r, fx2f(pPivotPoint->y) + Result.g); - IGraphics::CLineItem Line = IGraphics::CLineItem(Pos0.x, Pos0.y, Pos1.x, Pos1.y); - Graphics()->LinesDraw(&Line, 1); + IGraphics::CLineItem Line = IGraphics::CLineItem(Pos0.x, Pos0.y, Pos1.x, Pos1.y); + Graphics()->LinesDraw(&Line, 1); + + Pos0 = Pos1; + } } } Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); @@ -5520,7 +5527,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(pNewEnv) // add the default points { - if(pNewEnv->m_Channels == 4) + if(pNewEnv->GetChannels() == 4) { pNewEnv->AddPoint(0, f2fx(1.0f), f2fx(1.0f), f2fx(1.0f), f2fx(1.0f)); pNewEnv->AddPoint(1000, f2fx(1.0f), f2fx(1.0f), f2fx(1.0f), f2fx(1.0f)); @@ -5592,7 +5599,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } bool ShowColorBar = false; - if(pEnvelope && pEnvelope->m_Channels == 4) + if(pEnvelope && pEnvelope->GetChannels() == 4) { ShowColorBar = true; View.HSplitTop(20.0f, &ColorBar, &View); @@ -5604,7 +5611,6 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(pEnvelope) { - static std::vector s_vSelection; static int s_EnvelopeEditorID = 0; static int s_ActiveChannels = 0xf; @@ -5634,17 +5640,17 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) for(int i = 0; i < CEnvPoint::MAX_CHANNELS; i++, Bit <<= 1) { ToolBar.VSplitLeft(15.0f, &Button, &ToolBar); - if(i < pEnvelope->m_Channels) + if(i < pEnvelope->GetChannels()) { int Corners = IGraphics::CORNER_NONE; - if(pEnvelope->m_Channels == 1) + if(pEnvelope->GetChannels() == 1) Corners = IGraphics::CORNER_ALL; else if(i == 0) Corners = IGraphics::CORNER_L; - else if(i == pEnvelope->m_Channels - 1) + else if(i == pEnvelope->GetChannels() - 1) Corners = IGraphics::CORNER_R; - if(DoButton_Env(&s_aChannelButtons[i], s_aapNames[pEnvelope->m_Channels - 1][i], s_ActiveChannels & Bit, &Button, s_aapDescriptions[pEnvelope->m_Channels - 1][i], aColors[i], Corners)) + if(DoButton_Env(&s_aChannelButtons[i], s_aapNames[pEnvelope->GetChannels() - 1][i], s_ActiveChannels & Bit, &Button, s_aapDescriptions[pEnvelope->GetChannels() - 1][i], aColors[i], Corners)) s_ActiveChannels ^= Bit; } } @@ -5698,12 +5704,65 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) m_pTooltip = "Press right mouse button to create a new point"; } + // keep track of selected point to handle value/time text input + static const void *s_pSelectedPoint = nullptr; + + // render tangents for bezier curves + { + UI()->ClipEnable(&View); + Graphics()->TextureClear(); + Graphics()->LinesBegin(); + for(int c = 0; c < pEnvelope->GetChannels(); c++) + { + if(!(s_ActiveChannels & (1 << c))) + continue; + + for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++) + { + const float PosX = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; + const float PosY = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); + + // Out-Tangent + if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER) + { + const float OutTangentX = PosX + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] / 1000.0f / EndTime; + const float OutTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]) / (Top - Bottom); + + if(s_pSelectedPoint == &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f); + else + Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 0.4f); + + IGraphics::CLineItem LineItem(View.x + PosX * View.w, View.y + View.h - PosY * View.h, View.x + OutTangentX * View.w, View.y + View.h - OutTangentY * View.h); + Graphics()->LinesDraw(&LineItem, 1); + } + + // In-Tangent + if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER) + { + const float InTangentX = PosX + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] / 1000.0f / EndTime; + const float InTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]) / (Top - Bottom); + + if(s_pSelectedPoint == &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f); + else + Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 0.4f); + + IGraphics::CLineItem LineItem(View.x + PosX * View.w, View.y + View.h - PosY * View.h, View.x + InTangentX * View.w, View.y + View.h - InTangentY * View.h); + Graphics()->LinesDraw(&LineItem, 1); + } + } + } + Graphics()->LinesEnd(); + UI()->ClipDisable(); + } + // render lines { UI()->ClipEnable(&View); Graphics()->TextureClear(); Graphics()->LinesBegin(); - for(int c = 0; c < pEnvelope->m_Channels; c++) + for(int c = 0; c < pEnvelope->GetChannels(); c++) { if(s_ActiveChannels & (1 << c)) Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 1); @@ -5740,19 +5799,18 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) float t0 = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; float t1 = pEnvelope->m_vPoints[i + 1].m_Time / 1000.0f / EndTime; - CUIRect v; - v.x = CurveBar.x + (t0 + (t1 - t0) * 0.5f) * CurveBar.w; - v.y = CurveBar.y; - v.h = CurveBar.h; - v.w = CurveBar.h; - v.x -= v.w / 2; - void *pID = &pEnvelope->m_vPoints[i].m_Curvetype; - const char *apTypeName[] = { - "N", "L", "S", "F", "M"}; - const char *pTypeName = "Invalid"; + CUIRect CurveButton; + CurveButton.x = CurveBar.x + (t0 + (t1 - t0) * 0.5f) * CurveBar.w; + CurveButton.y = CurveBar.y; + CurveButton.h = CurveBar.h; + CurveButton.w = CurveBar.h; + CurveButton.x -= CurveButton.w / 2.0f; + const void *pID = &pEnvelope->m_vPoints[i].m_Curvetype; + const char *apTypeName[] = {"N", "L", "S", "F", "M", "B"}; + const char *pTypeName = "!?"; if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(apTypeName)) pTypeName = apTypeName[pEnvelope->m_vPoints[i].m_Curvetype]; - if(DoButton_Editor(pID, pTypeName, 0, &v, 0, "Switch curve type")) + if(DoButton_Editor(pID, pTypeName, 0, &CurveButton, 0, "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)")) pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES; } } @@ -5790,15 +5848,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) // render handles - // keep track of last Env - static void *s_pID = nullptr; - static CLineInputNumber s_CurValueInput; static CLineInputNumber s_CurTimeInput; if(CurrentEnvelopeSwitched) { - s_pID = nullptr; + s_pSelectedPoint = nullptr; // update displayed text s_CurValueInput.SetFloat(0.0f); @@ -5806,149 +5861,351 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } { - int CurrentValue = 0, CurrentTime = 0; - Graphics()->TextureClear(); Graphics()->QuadsBegin(); - for(int c = 0; c < pEnvelope->m_Channels; c++) + for(int c = 0; c < pEnvelope->GetChannels(); c++) { if(!(s_ActiveChannels & (1 << c))) continue; for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++) { - float x0 = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; - float y0 = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); - CUIRect Final; - Final.x = View.x + x0 * View.w; - Final.y = View.y + View.h - y0 * View.h; - Final.x -= 2.0f; - Final.y -= 2.0f; - Final.w = 4.0f; - Final.h = 4.0f; + // point handle + { + const float PosX = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; + const float PosY = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); + CUIRect Final; + Final.x = View.x + PosX * View.w; + Final.y = View.y + View.h - PosY * View.h; + Final.x -= 2.0f; + Final.y -= 2.0f; + Final.w = 4.0f; + Final.h = 4.0f; - void *pID = &pEnvelope->m_vPoints[i].m_aValues[c]; + const void *pID = &pEnvelope->m_vPoints[i].m_aValues[c]; - if(UI()->MouseInside(&Final)) - UI()->SetHotItem(pID); + if(UI()->MouseInside(&Final)) + UI()->SetHotItem(pID); - float ColorMod = 1.0f; + float ColorMod = 1.0f; - if(UI()->CheckActiveItem(pID)) - { - if(!UI()->MouseButton(0)) + if(UI()->CheckActiveItem(pID)) { - m_SelectedQuadEnvelope = -1; - m_SelectedEnvelopePoint = -1; + if(!UI()->MouseButton(0)) + { + m_SelectedQuadEnvelope = -1; + m_SelectedEnvelopePoint = -1; - UI()->SetActiveItem(nullptr); + UI()->SetActiveItem(nullptr); + } + else + { + if(Input()->ShiftIsPressed()) + { + if(i != 0) + { + float DeltaX = m_MouseDeltaX * TimeScale * (Input()->ModifierIsPressed() ? 1.0f : 1000.0f); + DeltaX = DeltaX < 0 ? -std::ceil(-DeltaX) : std::ceil(DeltaX); + pEnvelope->m_vPoints[i].m_Time += (int)DeltaX; + + if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; + if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; + } + } + else + { + pEnvelope->m_vPoints[i].m_aValues[c] -= f2fx(m_MouseDeltaY * (Input()->ModifierIsPressed() ? 0.001f : ValueScale)); + } + + m_SelectedQuadEnvelope = m_SelectedEnvelope; + m_ShowEnvelopePreview = SHOWENV_SELECTED; + m_SelectedEnvelopePoint = i; + m_Map.OnModify(); + } + + ColorMod = 100.0f; + Graphics()->SetColor(1, 1, 1, 1); } - else + else if(UI()->HotItem() == pID) { - if(Input()->ShiftIsPressed()) + if(UI()->MouseButton(0)) { - if(i != 0) + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + } + + // remove point + if(UI()->MouseButtonClicked(1)) + { + if(s_pSelectedPoint == pID) { - if(Input()->ModifierIsPressed()) - pEnvelope->m_vPoints[i].m_Time += (int)((m_MouseDeltaX)); - else - pEnvelope->m_vPoints[i].m_Time += (int)((m_MouseDeltaX * TimeScale) * 1000.0f); - if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; - if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; + s_pSelectedPoint = nullptr; + + // update displayed text + s_CurValueInput.SetFloat(0.0f); + s_CurTimeInput.SetFloat(0.0f); } + + pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + i); + m_Map.OnModify(); } - else + + m_ShowEnvelopePreview = SHOWENV_SELECTED; + ColorMod = 100.0f; + Graphics()->SetColor(1, 0.75f, 0.75f, 1); + m_pTooltip = "Envelope point. Left mouse to drag. Hold ctrl to be more precise. Hold shift to alter time point instead of value. Right click to delete."; + } + + if(pID == s_pSelectedPoint && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + { + if(i != 0) { - if(Input()->ModifierIsPressed()) - pEnvelope->m_vPoints[i].m_aValues[c] -= f2fx(m_MouseDeltaY * 0.001f); - else - pEnvelope->m_vPoints[i].m_aValues[c] -= f2fx(m_MouseDeltaY * ValueScale); + pEnvelope->m_vPoints[i].m_Time = s_CurTimeInput.GetFloat() * 1000.0f; + + if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; + if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; } + else + pEnvelope->m_vPoints[i].m_Time = 0.0f; - m_SelectedQuadEnvelope = m_SelectedEnvelope; - m_ShowEnvelopePreview = SHOWENV_SELECTED; - m_SelectedEnvelopePoint = i; - m_Map.OnModify(); + s_CurTimeInput.SetFloat(pEnvelope->m_vPoints[i].m_Time / 1000.0f); + + pEnvelope->m_vPoints[i].m_aValues[c] = f2fx(s_CurValueInput.GetFloat()); + s_CurValueInput.SetFloat(fx2f(pEnvelope->m_vPoints[i].m_aValues[c])); } - ColorMod = 100.0f; - Graphics()->SetColor(1, 1, 1, 1); - } - else if(UI()->HotItem() == pID) - { - if(UI()->MouseButton(0)) + if(UI()->CheckActiveItem(pID)) { - s_vSelection.clear(); - s_vSelection.push_back(i); - UI()->SetActiveItem(pID); - // track it - s_pID = pID; + const int CurrentTime = pEnvelope->m_vPoints[i].m_Time; + const int CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c]; + + // update displayed text + s_CurValueInput.SetFloat(fx2f(CurrentValue)); + s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); } - // remove point - if(UI()->MouseButtonClicked(1)) + if(pID == s_pSelectedPoint || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + else + Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 1.0f); + IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h); + Graphics()->QuadsDrawTL(&QuadItem, 1); + } + + // tangent handles for bezier curves + { + const float PosX = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; + const float PosY = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); + + // Out-Tangent handle + if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER) { - if(s_pID == pID) + const float OutTangentX = PosX + (pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] / 1000.0f / EndTime); + const float OutTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]) / (Top - Bottom); + + CUIRect Final; + Final.x = View.x + OutTangentX * View.w; + Final.y = View.y + View.h - OutTangentY * View.h; + Final.x -= 2.0f; + Final.y -= 2.0f; + Final.w = 4.0f; + Final.h = 4.0f; + + // handle logic + bool Updated = false; + const void *pID = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]; + + float ColorMod = 1.0f; + if(UI()->MouseInside(&Final)) + UI()->SetHotItem(pID); + + if(UI()->CheckActiveItem(pID)) + { + if(!UI()->MouseButton(0)) + { + m_SelectedQuadEnvelope = -1; + m_SelectedEnvelopePoint = -1; + + UI()->SetActiveItem(nullptr); + } + else + { + float DeltaX = m_MouseDeltaX * TimeScale * (Input()->ModifierIsPressed() ? 1.0f : 1000.0f); + DeltaX = DeltaX < 0 ? -std::ceil(-DeltaX) : std::ceil(DeltaX); + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] += (int)DeltaX; + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] -= f2fx(m_MouseDeltaY * (Input()->ModifierIsPressed() ? 0.005f : ValueScale)); + + // clamp time value + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = clamp(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c], 0, EndTime * 1000.0f - pEnvelope->m_vPoints[i].m_Time); + + m_SelectedQuadEnvelope = m_SelectedEnvelope; + m_ShowEnvelopePreview = SHOWENV_SELECTED; + m_SelectedEnvelopePoint = i; + m_Map.OnModify(); + } + ColorMod = 100.0f; + } + else if(UI()->HotItem() == pID) { - s_pID = nullptr; + if(UI()->MouseButton(0)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + } + + // reset + if(UI()->MouseButtonClicked(1)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX)); + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY)); + m_Map.OnModify(); + Updated = true; + } + + m_ShowEnvelopePreview = SHOWENV_SELECTED; + ColorMod = 100.0f; + m_pTooltip = "Bezier out-tangent. Left mouse to drag. Hold ctrl to be more precise. Right click to reset."; + } + + if(pID == s_pSelectedPoint && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + { + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = clamp(s_CurTimeInput.GetFloat() * 1000.0f - pEnvelope->m_vPoints[i].m_Time, 0, EndTime * 1000.0f - pEnvelope->m_vPoints[i].m_Time); + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = f2fx(s_CurValueInput.GetFloat()) - pEnvelope->m_vPoints[i].m_aValues[c]; + Updated = true; + } + + if(UI()->CheckActiveItem(pID) || Updated) + { + const int CurrentTime = pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]; + const int CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]; // update displayed text - s_CurValueInput.SetFloat(0.0f); - s_CurTimeInput.SetFloat(0.0f); + s_CurValueInput.SetFloat(fx2f(CurrentValue)); + s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); } - pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + i); - m_Map.OnModify(); - } + if(pID == s_pSelectedPoint || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); + else + Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 0.5f); - m_ShowEnvelopePreview = SHOWENV_SELECTED; - ColorMod = 100.0f; - Graphics()->SetColor(1, 0.75f, 0.75f, 1); - m_pTooltip = "Left mouse to drag. Hold ctrl to be more precise. Hold shift to alter time point as well. Right click to delete."; - } + // draw triangle + IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h); + Graphics()->QuadsDrawFreeform(&FreeformItem, 1); + } - if(pID == s_pID && (Input()->KeyIsPressed(KEY_RETURN) || Input()->KeyIsPressed(KEY_KP_ENTER))) - { - if(i != 0) + // In-Tangent handle + if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER) { - pEnvelope->m_vPoints[i].m_Time = s_CurTimeInput.GetFloat() * 1000.0f; + const float InTangentX = PosX + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] / 1000.0f / EndTime; + const float InTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]) / (Top - Bottom); + + CUIRect Final; + Final.x = View.x + InTangentX * View.w; + Final.y = View.y + View.h - InTangentY * View.h; + Final.x -= 2.0f; + Final.y -= 2.0f; + Final.w = 4.0f; + Final.h = 4.0f; + + // handle logic + bool Updated = false; + const void *pID = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]; + + float ColorMod = 1.0f; + if(UI()->MouseInside(&Final)) + UI()->SetHotItem(pID); + + if(UI()->CheckActiveItem(pID)) + { + if(!UI()->MouseButton(0)) + { + m_SelectedQuadEnvelope = -1; + m_SelectedEnvelopePoint = -1; - if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; - if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; - } - else - pEnvelope->m_vPoints[i].m_Time = 0.0f; + UI()->SetActiveItem(nullptr); + } + else + { + float DeltaX = m_MouseDeltaX * TimeScale * (Input()->ModifierIsPressed() ? 1.0f : 1000.0f); + DeltaX = DeltaX < 0 ? -std::ceil(-DeltaX) : std::ceil(DeltaX); + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] += (int)DeltaX; + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] -= f2fx(m_MouseDeltaY * (Input()->ModifierIsPressed() ? 0.005f : ValueScale)); + + // clamp time value + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = clamp(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c], -pEnvelope->m_vPoints[i].m_Time, 0); + + m_SelectedQuadEnvelope = m_SelectedEnvelope; + m_ShowEnvelopePreview = SHOWENV_SELECTED; + m_SelectedEnvelopePoint = i; + m_Map.OnModify(); + } + ColorMod = 100.0f; + } + else if(UI()->HotItem() == pID) + { + if(UI()->MouseButton(0)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + } - s_CurTimeInput.SetFloat(pEnvelope->m_vPoints[i].m_Time / 1000.0f); + // reset + if(UI()->MouseButtonClicked(1)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX)); + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY)); + m_Map.OnModify(); + Updated = true; + } - pEnvelope->m_vPoints[i].m_aValues[c] = f2fx(s_CurValueInput.GetFloat()); - s_CurValueInput.SetFloat(fx2f(pEnvelope->m_vPoints[i].m_aValues[c])); - } + m_ShowEnvelopePreview = SHOWENV_SELECTED; + ColorMod = 100.0f; + m_pTooltip = "Bezier in-tangent. Left mouse to drag. Hold ctrl to be more precise. Right click to reset."; + } - if(UI()->CheckActiveItem(pID)) - { - CurrentTime = pEnvelope->m_vPoints[i].m_Time; - CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c]; + if(pID == s_pSelectedPoint && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + { + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = clamp(s_CurTimeInput.GetFloat() * 1000.0f - pEnvelope->m_vPoints[i].m_Time, -pEnvelope->m_vPoints[i].m_Time, 0); + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = f2fx(s_CurValueInput.GetFloat()) - pEnvelope->m_vPoints[i].m_aValues[c]; + Updated = true; + } - // update displayed text - s_CurValueInput.SetFloat(fx2f(CurrentValue)); - s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); - } + if(UI()->CheckActiveItem(pID) || Updated) + { + const int CurrentTime = pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]; + const int CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]; - if(m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i) - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); - else - Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 1.0f); - IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h); - Graphics()->QuadsDrawTL(&QuadItem, 1); + // update displayed text + s_CurValueInput.SetFloat(fx2f(CurrentValue)); + s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); + } + + if(pID == s_pSelectedPoint || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); + else + Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 0.5f); + + // draw triangle + IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h); + Graphics()->QuadsDrawFreeform(&FreeformItem, 1); + } + } } } Graphics()->QuadsEnd(); + } + if(s_pSelectedPoint != nullptr) + { CUIRect ToolBar1; CUIRect ToolBar2; ToolBar.VSplitMid(&ToolBar1, &ToolBar2); @@ -5968,8 +6225,8 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->DoLabel(&Label2, "Time (in s):", 10.0f, TEXTALIGN_MR); } - DoEditBox(&s_CurValueInput, &ToolBar1, 10.0f, IGraphics::CORNER_ALL, "The value of the selected envelope point"); - DoEditBox(&s_CurTimeInput, &ToolBar2, 10.0f, IGraphics::CORNER_ALL, "The time of the selected envelope point"); + DoEditBox(&s_CurValueInput, &ToolBar1, 10.0f, IGraphics::CORNER_ALL, "The value of the selected element"); + DoEditBox(&s_CurTimeInput, &ToolBar2, 10.0f, IGraphics::CORNER_ALL, "The time of the selected element"); } } } diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index bf39e27f887..95ce14ac41a 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -44,17 +44,50 @@ enum class CEnvelope { -public: + class CEnvelopePointAccess : public IEnvelopePointAccess + { + std::vector *m_pvPoints; + + public: + CEnvelopePointAccess(std::vector *pvPoints) + { + m_pvPoints = pvPoints; + } + + int NumPoints() const override + { + return m_pvPoints->size(); + } + + const CEnvPoint *GetPoint(int Index) const override + { + if(Index < 0 || (size_t)Index >= m_pvPoints->size()) + return nullptr; + return &m_pvPoints->at(Index); + } + + const CEnvPointBezier *GetBezier(int Index) const override + { + if(Index < 0 || (size_t)Index >= m_pvPoints->size()) + return nullptr; + return &m_pvPoints->at(Index).m_Bezier; + } + }; + int m_Channels; - std::vector m_vPoints; + +public: + std::vector m_vPoints; + CEnvelopePointAccess m_PointsAccess; char m_aName[32]; float m_Bottom, m_Top; bool m_Synchronized; - CEnvelope(int Chan) + CEnvelope(int Channels) : + m_PointsAccess(&m_vPoints) { - m_Channels = Chan; - m_aName[0] = 0; + SetChannels(Channels); + m_aName[0] = '\0'; m_Bottom = 0; m_Top = 0; m_Synchronized = false; @@ -70,46 +103,82 @@ class CEnvelope { m_Top = -1000000000.0f; m_Bottom = 1000000000.0f; + CEnvPoint_runtime *pPrevPoint = nullptr; for(auto &Point : m_vPoints) { for(int c = 0; c < m_Channels; c++) { if(ChannelMask & (1 << c)) { - float v = fx2f(Point.m_aValues[c]); - if(v > m_Top) - m_Top = v; - if(v < m_Bottom) - m_Bottom = v; + { + // value handle + const float v = fx2f(Point.m_aValues[c]); + m_Top = maximum(m_Top, v); + m_Bottom = minimum(m_Bottom, v); + } + + if(Point.m_Curvetype == CURVETYPE_BEZIER) + { + // out-tangent handle + const float v = fx2f(Point.m_aValues[c] + Point.m_Bezier.m_aOutTangentDeltaY[c]); + m_Top = maximum(m_Top, v); + m_Bottom = minimum(m_Bottom, v); + } + + if(pPrevPoint != nullptr && pPrevPoint->m_Curvetype == CURVETYPE_BEZIER) + { + // in-tangent handle + const float v = fx2f(Point.m_aValues[c] + Point.m_Bezier.m_aInTangentDeltaY[c]); + m_Top = maximum(m_Top, v); + m_Bottom = minimum(m_Bottom, v); + } } } + pPrevPoint = &Point; } } int Eval(float Time, ColorRGBA &Color) { - CRenderTools::RenderEvalEnvelope(&m_vPoints[0], m_vPoints.size(), m_Channels, std::chrono::nanoseconds((int64_t)((double)Time * (double)std::chrono::nanoseconds(1s).count())), Color); + CRenderTools::RenderEvalEnvelope(&m_PointsAccess, m_Channels, std::chrono::nanoseconds((int64_t)((double)Time * (double)std::chrono::nanoseconds(1s).count())), Color); return m_Channels; } void AddPoint(int Time, int v0, int v1 = 0, int v2 = 0, int v3 = 0) { - CEnvPoint p; + CEnvPoint_runtime p; p.m_Time = Time; p.m_aValues[0] = v0; p.m_aValues[1] = v1; p.m_aValues[2] = v2; p.m_aValues[3] = v3; p.m_Curvetype = CURVETYPE_LINEAR; + for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++) + { + p.m_Bezier.m_aInTangentDeltaX[c] = 0; + p.m_Bezier.m_aInTangentDeltaY[c] = 0; + p.m_Bezier.m_aOutTangentDeltaX[c] = 0; + p.m_Bezier.m_aOutTangentDeltaY[c] = 0; + } m_vPoints.push_back(p); Resort(); } float EndTime() const { - if(!m_vPoints.empty()) - return m_vPoints[m_vPoints.size() - 1].m_Time * (1.0f / 1000.0f); - return 0; + if(m_vPoints.empty()) + return 0.0f; + return m_vPoints.back().m_Time / 1000.0f; + } + + int GetChannels() const + { + return m_Channels; + } + + void SetChannels(int Channels) + { + m_Channels = clamp(Channels, 1, CEnvPoint::MAX_CHANNELS); } }; @@ -623,6 +692,7 @@ class CLayerTiles : public CLayer void ModifyEnvelopeIndex(FIndexModifyFunction pfnFunc) override; void PrepareForSave(); + void ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize); void GetSize(float *pWidth, float *pHeight) override { diff --git a/src/game/editor/io.cpp b/src/game/editor/io.cpp index a8eefca3031..6f660b92c80 100644 --- a/src/game/editor/io.cpp +++ b/src/game/editor/io.cpp @@ -56,7 +56,7 @@ bool CEditorMap::Save(const char *pFileName) // save version { CMapItemVersion Item; - Item.m_Version = 1; + Item.m_Version = CMapItemVersion::CURRENT_VERSION; df.AddItem(MAPITEMTYPE_VERSION, 0, sizeof(Item), &Item); } @@ -116,14 +116,16 @@ bool CEditorMap::Save(const char *pFileName) pImg->AnalyseTileFlags(); CMapItemImage Item; - Item.m_Version = 1; + Item.m_Version = CMapItemImage::CURRENT_VERSION; Item.m_Width = pImg->m_Width; Item.m_Height = pImg->m_Height; Item.m_External = pImg->m_External; Item.m_ImageName = df.AddData(str_length(pImg->m_aName) + 1, pImg->m_aName); if(pImg->m_External) + { Item.m_ImageData = -1; + } else { if(pImg->m_Format == CImageInfo::FORMAT_RGB) @@ -201,7 +203,7 @@ bool CEditorMap::Save(const char *pFileName) pLayerTiles->PrepareForSave(); CMapItemLayerTilemap Item; - Item.m_Version = 3; + Item.m_Version = CMapItemLayerTilemap::CURRENT_VERSION; Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0 Item.m_Layer.m_Flags = pLayerTiles->m_Flags; @@ -345,12 +347,13 @@ bool CEditorMap::Save(const char *pFileName) } // save envelopes + m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", "saving envelopes"); int PointCount = 0; for(size_t e = 0; e < m_vpEnvelopes.size(); e++) { CMapItemEnvelope Item; Item.m_Version = CMapItemEnvelope::CURRENT_VERSION; - Item.m_Channels = m_vpEnvelopes[e]->m_Channels; + Item.m_Channels = m_vpEnvelopes[e]->GetChannels(); Item.m_StartPoint = PointCount; Item.m_NumPoints = m_vpEnvelopes[e]->m_vPoints.size(); Item.m_Synchronized = m_vpEnvelopes[e]->m_Synchronized; @@ -361,20 +364,61 @@ bool CEditorMap::Save(const char *pFileName) } // save points - int TotalSize = sizeof(CEnvPoint) * PointCount; - CEnvPoint *pPoints = (CEnvPoint *)calloc(maximum(PointCount, 1), sizeof(*pPoints)); + m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", "saving envelope points"); + bool BezierUsed = true; + for(const auto &pEnvelope : m_vpEnvelopes) + { + for(const auto &Point : pEnvelope->m_vPoints) + { + if(Point.m_Curvetype == CURVETYPE_BEZIER) + { + BezierUsed = true; + break; + } + } + if(BezierUsed) + break; + } + + CEnvPoint *pPoints = (CEnvPoint *)calloc(maximum(PointCount, 1), sizeof(CEnvPoint)); + CEnvPointBezier *pPointsBezier = nullptr; + if(BezierUsed) + pPointsBezier = (CEnvPointBezier *)calloc(maximum(PointCount, 1), sizeof(CEnvPointBezier)); PointCount = 0; for(const auto &pEnvelope : m_vpEnvelopes) { - int Count = pEnvelope->m_vPoints.size(); - mem_copy(&pPoints[PointCount], pEnvelope->m_vPoints.data(), sizeof(CEnvPoint) * Count); - PointCount += Count; + const CEnvPoint_runtime *pPrevPoint = nullptr; + for(const auto &Point : pEnvelope->m_vPoints) + { + mem_copy(&pPoints[PointCount], &Point, sizeof(CEnvPoint)); + if(pPointsBezier != nullptr) + { + if(Point.m_Curvetype == CURVETYPE_BEZIER) + { + mem_copy(&pPointsBezier[PointCount].m_aOutTangentDeltaX, &Point.m_Bezier.m_aOutTangentDeltaX, sizeof(Point.m_Bezier.m_aOutTangentDeltaX)); + mem_copy(&pPointsBezier[PointCount].m_aOutTangentDeltaY, &Point.m_Bezier.m_aOutTangentDeltaY, sizeof(Point.m_Bezier.m_aOutTangentDeltaY)); + } + if(pPrevPoint != nullptr && pPrevPoint->m_Curvetype == CURVETYPE_BEZIER) + { + mem_copy(&pPointsBezier[PointCount].m_aInTangentDeltaX, &Point.m_Bezier.m_aInTangentDeltaX, sizeof(Point.m_Bezier.m_aInTangentDeltaX)); + mem_copy(&pPointsBezier[PointCount].m_aInTangentDeltaY, &Point.m_Bezier.m_aInTangentDeltaY, sizeof(Point.m_Bezier.m_aInTangentDeltaY)); + } + } + PointCount++; + pPrevPoint = &Point; + } } - df.AddItem(MAPITEMTYPE_ENVPOINTS, 0, TotalSize, pPoints); + df.AddItem(MAPITEMTYPE_ENVPOINTS, 0, sizeof(CEnvPoint) * PointCount, pPoints); free(pPoints); + if(pPointsBezier != nullptr) + { + df.AddItem(MAPITEMTYPE_ENVPOINTS_BEZIER, 0, sizeof(CEnvPointBezier) * PointCount, pPointsBezier); + free(pPointsBezier); + } + // finish the data file std::shared_ptr pWriterFinishJob = std::make_shared(pFileName, std::move(df)); m_pEditor->Engine()->AddJob(pWriterFinishJob); @@ -421,7 +465,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio { return false; } - else if(pItemVersion->m_Version == 1) + else if(pItemVersion->m_Version == CMapItemVersion::CURRENT_VERSION) { // load map info { @@ -470,14 +514,15 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio DataFile.GetType(MAPITEMTYPE_IMAGE, &Start, &Num); for(int i = 0; i < Num; i++) { - CMapItemImage *pItem = (CMapItemImage *)DataFile.GetItem(Start + i); + CMapItemImage_v2 *pItem = (CMapItemImage_v2 *)DataFile.GetItem(Start + i); char *pName = (char *)DataFile.GetData(pItem->m_ImageName); // copy base info CEditorImage *pImg = new CEditorImage(m_pEditor); pImg->m_External = pItem->m_External; - if(pItem->m_External) + const int Format = pItem->m_Version < CMapItemImage_v2::CURRENT_VERSION ? CImageInfo::FORMAT_RGBA : pItem->m_Format; + if(pImg->m_External || (Format != CImageInfo::FORMAT_RGB && Format != CImageInfo::FORMAT_RGBA)) { char aBuf[IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "mapres/%s.png", pName); @@ -499,7 +544,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio { pImg->m_Width = pItem->m_Width; pImg->m_Height = pItem->m_Height; - pImg->m_Format = CImageInfo::FORMAT_RGBA; + pImg->m_Format = Format; // copy image data void *pData = DataFile.GetData(pItem->m_ImageData); @@ -736,9 +781,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio { void *pFrontData = DataFile.GetData(pTilemapItem->m_Front); unsigned int Size = DataFile.GetDataSize(pTilemapItem->m_Front); - if(Size >= (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTile)) - mem_copy(((CLayerFront *)pTiles)->m_pTiles, pFrontData, (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTile)); - + pTiles->ExtractTiles(pTilemapItem->m_Version, (CTile *)pFrontData, Size); DataFile.UnloadData(pTilemapItem->m_Front); } else if(pTiles->m_Switch) @@ -793,17 +836,14 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio { void *pData = DataFile.GetData(pTilemapItem->m_Data); unsigned int Size = DataFile.GetDataSize(pTilemapItem->m_Data); - if(Size >= (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTile)) - { - mem_copy(pTiles->m_pTiles, pData, (size_t)pTiles->m_Width * pTiles->m_Height * sizeof(CTile)); + pTiles->ExtractTiles(pTilemapItem->m_Version, (CTile *)pData, Size); - if(pTiles->m_Game && pTilemapItem->m_Version == MakeVersion(1, *pTilemapItem)) + if(pTiles->m_Game && pTilemapItem->m_Version == MakeVersion(1, *pTilemapItem)) + { + for(int i = 0; i < pTiles->m_Width * pTiles->m_Height; i++) { - for(int i = 0; i < pTiles->m_Width * pTiles->m_Height; i++) - { - if(pTiles->m_pTiles[i].m_Index) - pTiles->m_pTiles[i].m_Index += ENTITY_OFFSET; - } + if(pTiles->m_pTiles[i].m_Index) + pTiles->m_pTiles[i].m_Index += ENTITY_OFFSET; } } DataFile.UnloadData(pTilemapItem->m_Data); @@ -909,37 +949,38 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio // load envelopes { - CEnvPoint *pPoints = nullptr; + const CMapBasedEnvelopePointAccess EnvelopePoints(&DataFile); + int EnvStart, EnvNum; + DataFile.GetType(MAPITEMTYPE_ENVELOPE, &EnvStart, &EnvNum); + for(int e = 0; e < EnvNum; e++) { - int Start, Num; - DataFile.GetType(MAPITEMTYPE_ENVPOINTS, &Start, &Num); - if(Num) - pPoints = (CEnvPoint *)DataFile.GetItem(Start); - } - - int Start, Num; - DataFile.GetType(MAPITEMTYPE_ENVELOPE, &Start, &Num); - for(int e = 0; e < Num; e++) - { - CMapItemEnvelope *pItem = (CMapItemEnvelope *)DataFile.GetItem(Start + e); + CMapItemEnvelope *pItem = (CMapItemEnvelope *)DataFile.GetItem(EnvStart + e); CEnvelope *pEnv = new CEnvelope(pItem->m_Channels); pEnv->m_vPoints.resize(pItem->m_NumPoints); - mem_copy(pEnv->m_vPoints.data(), &pPoints[pItem->m_StartPoint], sizeof(CEnvPoint) * pItem->m_NumPoints); + for(int p = 0; p < pItem->m_NumPoints; p++) + { + const CEnvPoint *pPoint = EnvelopePoints.GetPoint(pItem->m_StartPoint + p); + if(pPoint != nullptr) + mem_copy(&pEnv->m_vPoints[p], pPoint, sizeof(CEnvPoint)); + const CEnvPointBezier *pPointBezier = EnvelopePoints.GetBezier(pItem->m_StartPoint + p); + if(pPointBezier != nullptr) + mem_copy(&pEnv->m_vPoints[p].m_Bezier, pPointBezier, sizeof(CEnvPointBezier)); + } if(pItem->m_aName[0] != -1) // compatibility with old maps IntsToStr(pItem->m_aName, sizeof(pItem->m_aName) / sizeof(int), pEnv->m_aName); m_vpEnvelopes.push_back(pEnv); - if(pItem->m_Version >= 2) + if(pItem->m_Version >= CMapItemEnvelope_v2::CURRENT_VERSION) pEnv->m_Synchronized = pItem->m_Synchronized; } } { - int Start, Num; - DataFile.GetType(MAPITEMTYPE_AUTOMAPPER_CONFIG, &Start, &Num); - for(int i = 0; i < Num; i++) + int AutomapperConfigStart, AutomapperConfigNum; + DataFile.GetType(MAPITEMTYPE_AUTOMAPPER_CONFIG, &AutomapperConfigStart, &AutomapperConfigNum); + for(int i = 0; i < AutomapperConfigNum; i++) { - CMapItemAutoMapperConfig *pItem = (CMapItemAutoMapperConfig *)DataFile.GetItem(Start + i); + CMapItemAutoMapperConfig *pItem = (CMapItemAutoMapperConfig *)DataFile.GetItem(AutomapperConfigStart + i); if(pItem->m_Version == CMapItemAutoMapperConfig::CURRENT_VERSION) { if(pItem->m_GroupId >= 0 && (size_t)pItem->m_GroupId < m_vpGroups.size() && diff --git a/src/game/editor/layer_tiles.cpp b/src/game/editor/layer_tiles.cpp index f5cf66c6797..3acd941af16 100644 --- a/src/game/editor/layer_tiles.cpp +++ b/src/game/editor/layer_tiles.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "editor.h" @@ -97,6 +98,15 @@ void CLayerTiles::PrepareForSave() } } +void CLayerTiles::ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize) +{ + const size_t DestSize = (size_t)m_Width * m_Height; + if(TilemapItemVersion >= CMapItemLayerTilemap::TILE_SKIP_MIN_VERSION) + CMap::ExtractTiles(m_pTiles, DestSize, pSavedTiles, SavedTilesSize); + else if(SavedTilesSize >= DestSize) + mem_copy(m_pTiles, pSavedTiles, DestSize * sizeof(CTile)); +} + void CLayerTiles::MakePalette() { for(int y = 0; y < m_Height; y++) @@ -920,7 +930,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) { for(; Index >= -1 && Index < (int)m_pEditor->m_Map.m_vpEnvelopes.size(); Index += Step) { - if(Index == -1 || m_pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 4) + if(Index == -1 || m_pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 4) { m_ColorEnv = Index; break; diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 146e3dcd711..7145e47a888 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -936,7 +936,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b { for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 3) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 3) { pQuad->m_PosEnv = Index; break; @@ -956,7 +956,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b { for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 4) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 4) { pQuad->m_ColorEnv = Index; break; @@ -1095,7 +1095,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, const int StepDirection = Index < pSource->m_PosEnv ? -1 : 1; for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 3) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 3) { pSource->m_PosEnv = Index; break; @@ -1112,7 +1112,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, const int StepDirection = Index < pSource->m_SoundEnv ? -1 : 1; for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 1) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 1) { pSource->m_SoundEnv = Index; break; diff --git a/src/game/mapitems.h b/src/game/mapitems.h index efd7e00900d..8e4292a6f23 100644 --- a/src/game/mapitems.h +++ b/src/game/mapitems.h @@ -37,6 +37,7 @@ enum CURVETYPE_SLOW, CURVETYPE_FAST, CURVETYPE_SMOOTH, + CURVETYPE_BEZIER, NUM_CURVETYPES, // game layer tiles @@ -251,8 +252,13 @@ struct CMapItemInfoSettings : CMapItemInfo int m_Settings; }; -struct CMapItemImage +struct CMapItemImage_v1 { + enum + { + CURRENT_VERSION = 1, + }; + int m_Version; int m_Width; int m_Height; @@ -261,6 +267,18 @@ struct CMapItemImage int m_ImageData; }; +struct CMapItemImage_v2 : public CMapItemImage_v1 +{ + enum + { + CURRENT_VERSION = 2, + }; + + int m_Format; // Default before this version is CImageInfo::FORMAT_RGBA +}; + +typedef CMapItemImage_v1 CMapItemImage; + struct CMapItemGroup_v1 { int m_Version; @@ -298,6 +316,12 @@ struct CMapItemLayer struct CMapItemLayerTilemap { + enum + { + CURRENT_VERSION = 3, + TILE_SKIP_MIN_VERSION = 4, // supported for loading but not saving + }; + CMapItemLayer m_Layer; int m_Version; @@ -337,9 +361,16 @@ struct CMapItemLayerQuads struct CMapItemVersion { + enum + { + CURRENT_VERSION = 1 + }; + int m_Version; }; +// Represents basic information about envelope points. +// In upstream Teeworlds, this is only used if all CMapItemEnvelope are version 1 or 2. struct CEnvPoint { enum @@ -348,14 +379,45 @@ struct CEnvPoint }; int m_Time; // in ms - int m_Curvetype; + int m_Curvetype; // CURVETYPE_* constants, any unknown value behaves like CURVETYPE_LINEAR int m_aValues[MAX_CHANNELS]; // 1-4 depending on envelope (22.10 fixed point) bool operator<(const CEnvPoint &Other) const { return m_Time < Other.m_Time; } }; +// Represents additional envelope point information for CURVETYPE_BEZIER. +// In DDNet, these are stored separately in an UUID-based map item. +// In upstream Teeworlds, CEnvPointBezier_upstream is used instead. +struct CEnvPointBezier +{ + // DeltaX in ms and DeltaY as 22.10 fxp + int m_aInTangentDeltaX[CEnvPoint::MAX_CHANNELS]; + int m_aInTangentDeltaY[CEnvPoint::MAX_CHANNELS]; + int m_aOutTangentDeltaX[CEnvPoint::MAX_CHANNELS]; + int m_aOutTangentDeltaY[CEnvPoint::MAX_CHANNELS]; +}; + +// Written to maps on upstream Teeworlds for envelope points including bezier information instead of the basic +// CEnvPoint items, if at least one CMapItemEnvelope with version 3 or higher exists in the map. +struct CEnvPointBezier_upstream : public CEnvPoint +{ + CEnvPointBezier m_Bezier; +}; + +// Used to represent all envelope point information at runtime in editor. +// (Can eventually be different than CEnvPointBezier_upstream) +struct CEnvPoint_runtime : public CEnvPoint +{ + CEnvPointBezier m_Bezier; +}; + struct CMapItemEnvelope_v1 { + enum + { + CURRENT_VERSION = 1, + }; + int m_Version; int m_Channels; int m_StartPoint; @@ -363,15 +425,29 @@ struct CMapItemEnvelope_v1 int m_aName[8]; }; -struct CMapItemEnvelope : public CMapItemEnvelope_v1 +struct CMapItemEnvelope_v2 : public CMapItemEnvelope_v1 { enum { - CURRENT_VERSION = 2 + CURRENT_VERSION = 2, }; + int m_Synchronized; }; +// Only written to maps in upstream Teeworlds. +// If at least one of these exists in a map, the envelope points +// are represented by CEnvPointBezier_upstream instead of CEnvPoint. +struct CMapItemEnvelope_v3 : public CMapItemEnvelope_v2 +{ + enum + { + CURRENT_VERSION = 3, + }; +}; + +typedef CMapItemEnvelope_v2 CMapItemEnvelope; + struct CSoundShape { enum diff --git a/src/game/mapitems_ex_types.h b/src/game/mapitems_ex_types.h index 304d4e5d189..2c686912950 100644 --- a/src/game/mapitems_ex_types.h +++ b/src/game/mapitems_ex_types.h @@ -3,3 +3,4 @@ UUID(MAPITEMTYPE_TEST, "mapitemtype-test@ddnet.tw") UUID(MAPITEMTYPE_AUTOMAPPER_CONFIG, "mapitemtype-automapper-config@ddnet.tw") UUID(MAPITEMTYPE_GROUP_EX, "mapitemtype-group@ddnet.tw") +UUID(MAPITEMTYPE_ENVPOINTS_BEZIER, "mapitemtype-envpoints-bezier@ddnet.tw") diff --git a/src/tools/map_convert_07.cpp b/src/tools/map_convert_07.cpp index 68029c34074..679fde2f3bf 100644 --- a/src/tools/map_convert_07.cpp +++ b/src/tools/map_convert_07.cpp @@ -96,24 +96,25 @@ bool CheckImageDimensions(void *pLayerItem, int LayerType, const char *pFilename return false; } -void *ReplaceImageItem(void *pItem, int Type, CMapItemImage *pNewImgItem) +void *ReplaceImageItem(CMapItemImage *pImgItem, CMapItemImage *pNewImgItem) { - if(Type != MAPITEMTYPE_IMAGE) - return pItem; - - CMapItemImage *pImgItem = (CMapItemImage *)pItem; - if(!pImgItem->m_External) - return pItem; + return pImgItem; char *pName = (char *)g_DataReader.GetData(pImgItem->m_ImageName); dbg_msg("map_convert_07", "embedding image '%s'", pName); CImageInfo ImgInfo; - char aStr[64]; + char aStr[IO_MAX_PATH_LENGTH]; str_format(aStr, sizeof(aStr), "data/mapres/%s.png", pName); if(!LoadPNG(&ImgInfo, aStr)) - return pItem; // keep as external if we don't have a mapres to replace + return pImgItem; // keep as external if we don't have a mapres to replace + + if(ImgInfo.m_Format != CImageInfo::FORMAT_RGBA) + { + dbg_msg("map_convert_07", "image '%s' is not in RGBA format", aStr); + return pImgItem; + } *pNewImgItem = *pImgItem; @@ -217,9 +218,14 @@ int main(int argc, const char **argv) Success &= CheckImageDimensions(pItem, Type, pSourceFileName); CMapItemImage NewImageItem; - pItem = ReplaceImageItem(pItem, Type, &NewImageItem); - if(!pItem) - return -1; + if(Type == MAPITEMTYPE_IMAGE) + { + pItem = ReplaceImageItem((CMapItemImage *)pItem, &NewImageItem); + if(!pItem) + return -1; + Size = sizeof(CMapItemImage); + NewImageItem.m_Version = CMapItemImage::CURRENT_VERSION; + } g_DataWriter.AddItem(Type, ID, Size, pItem); } diff --git a/src/tools/map_extract.cpp b/src/tools/map_extract.cpp index c74754cc590..a6902285420 100644 --- a/src/tools/map_extract.cpp +++ b/src/tools/map_extract.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,7 @@ bool Process(IStorage *pStorage, const char *pMapName, const char *pPathSave) for(int i = 0; i < Num; i++) { - CMapItemImage *pItem = (CMapItemImage *)Reader.GetItem(Start + i); + CMapItemImage_v2 *pItem = (CMapItemImage_v2 *)Reader.GetItem(Start + i); char *pName = (char *)Reader.GetData(pItem->m_ImageName); if(pItem->m_External) @@ -49,6 +50,18 @@ bool Process(IStorage *pStorage, const char *pMapName, const char *pPathSave) str_format(aBuf, sizeof(aBuf), "%s/%s.png", pPathSave, pName); dbg_msg("map_extract", "writing image: %s (%dx%d)", aBuf, pItem->m_Width, pItem->m_Height); + const int Format = pItem->m_Version < CMapItemImage_v2::CURRENT_VERSION ? CImageInfo::FORMAT_RGBA : pItem->m_Format; + EImageFormat OutputFormat; + if(Format == CImageInfo::FORMAT_RGBA) + OutputFormat = IMAGE_FORMAT_RGBA; + else if(Format == CImageInfo::FORMAT_RGB) + OutputFormat = IMAGE_FORMAT_RGB; + else + { + dbg_msg("map_extract", "ignoring image '%s' with unknown format %d", aBuf, Format); + continue; + } + // copy image data IOHANDLE File = io_open(aBuf, IOFLAG_WRITE); if(File) @@ -56,7 +69,7 @@ bool Process(IStorage *pStorage, const char *pMapName, const char *pPathSave) TImageByteBuffer ByteBuffer; SImageByteBuffer ImageByteBuffer(&ByteBuffer); - if(SavePNG(IMAGE_FORMAT_RGBA, (const uint8_t *)Reader.GetData(pItem->m_ImageData), ImageByteBuffer, pItem->m_Width, pItem->m_Height)) + if(SavePNG(OutputFormat, (const uint8_t *)Reader.GetData(pItem->m_ImageData), ImageByteBuffer, pItem->m_Width, pItem->m_Height)) io_write(File, &ByteBuffer.front(), ByteBuffer.size()); io_close(File); } diff --git a/src/tools/map_optimize.cpp b/src/tools/map_optimize.cpp index c0ac1b8bf19..4b7bbfa70d6 100644 --- a/src/tools/map_optimize.cpp +++ b/src/tools/map_optimize.cpp @@ -188,8 +188,8 @@ int main(int argc, const char **argv) } else if(Type == MAPITEMTYPE_IMAGE) { - CMapItemImage *pImg = (CMapItemImage *)pPtr; - if(!pImg->m_External) + CMapItemImage_v2 *pImg = (CMapItemImage_v2 *)pPtr; + if(!pImg->m_External && pImg->m_Version < CMapItemImage_v2::CURRENT_VERSION) { SMapOptimizeItem Item; Item.m_pImage = pImg; diff --git a/src/tools/map_replace_image.cpp b/src/tools/map_replace_image.cpp index 06baabcbfac..ca223526f0b 100644 --- a/src/tools/map_replace_image.cpp +++ b/src/tools/map_replace_image.cpp @@ -67,16 +67,12 @@ int LoadPNG(CImageInfo *pImg, const char *pFilename) return 1; } -void *ReplaceImageItem(void *pItem, int Type, const char *pImgName, const char *pImgFile, CMapItemImage *pNewImgItem) +void *ReplaceImageItem(CMapItemImage *pImgItem, const char *pImgName, const char *pImgFile, CMapItemImage *pNewImgItem) { - if(Type != MAPITEMTYPE_IMAGE) - return pItem; - - CMapItemImage *pImgItem = (CMapItemImage *)pItem; char *pName = (char *)g_DataReader.GetData(pImgItem->m_ImageName); if(str_comp(pImgName, pName) != 0) - return pItem; + return pImgItem; dbg_msg("map_replace_image", "found image '%s'", pImgName); @@ -84,17 +80,22 @@ void *ReplaceImageItem(void *pItem, int Type, const char *pImgName, const char * if(!LoadPNG(&ImgInfo, pImgFile)) return 0; + if(ImgInfo.m_Format != CImageInfo::FORMAT_RGBA) + { + dbg_msg("map_replace_image", "image '%s' is not in RGBA format", pImgName); + return 0; + } + *pNewImgItem = *pImgItem; pNewImgItem->m_Width = ImgInfo.m_Width; pNewImgItem->m_Height = ImgInfo.m_Height; - int PixelSize = ImgInfo.m_Format == CImageInfo::FORMAT_RGB ? 3 : 4; g_NewNameID = pImgItem->m_ImageName; IStorage::StripPathAndExtension(pImgFile, g_aNewName, sizeof(g_aNewName)); g_NewDataID = pImgItem->m_ImageData; g_pNewData = ImgInfo.m_pData; - g_NewDataSize = ImgInfo.m_Width * ImgInfo.m_Height * PixelSize; + g_NewDataSize = ImgInfo.m_Width * ImgInfo.m_Height * 4; return (void *)pNewImgItem; } @@ -148,12 +149,18 @@ int main(int argc, const char **argv) if(Type == ITEMTYPE_EX) continue; + int Size = g_DataReader.GetItemSize(Index); + CMapItemImage NewImageItem; - pItem = ReplaceImageItem(pItem, Type, pImageName, pImageFile, &NewImageItem); - if(!pItem) - return -1; + if(Type == MAPITEMTYPE_IMAGE) + { + pItem = ReplaceImageItem((CMapItemImage *)pItem, pImageName, pImageFile, &NewImageItem); + if(!pItem) + return -1; + Size = sizeof(CMapItemImage); + NewImageItem.m_Version = CMapItemImage::CURRENT_VERSION; + } - int Size = g_DataReader.GetItemSize(Index); Writer.AddItem(Type, ID, Size, pItem); }