diff --git a/CGame_Render.cpp b/CGame_Render.cpp index 3fb5b4b..63b82aa 100644 --- a/CGame_Render.cpp +++ b/CGame_Render.cpp @@ -55,6 +55,8 @@ void CGame::Render() // render the map if active if(MapObj && MapObj->isActive()) { + if(MapObj->getMap()) + CSurface::UpdatePaletteAnimations(MapObj->getMap()->type); if(auto* mapSurf = MapObj->getSurface()) { std::array textBuffer; diff --git a/CIO/CFile.cpp b/CIO/CFile.cpp index 078d1d0..8e6c89b 100644 --- a/CIO/CFile.cpp +++ b/CIO/CFile.cpp @@ -348,6 +348,8 @@ bool CFile::read_lbm(FILE* fp, const boost::filesystem::path& filepath) // skip unknown data (length - 20 x 1 Byte) // fseek(fp, length-20, SEEK_CUR); + const Uint16 tilesetSlot = static_cast(bmpArray - global::bmpArray.data()); + /* READ SECOND CHUNK "CMAP" */ // chunk-identifier (4 Bytes) @@ -392,7 +394,55 @@ bool CFile::read_lbm(FILE* fp, const boost::filesystem::path& filepath) if(strcmp(chunk_identifier.data(), "BODY") == 0) break; - else + else if(strcmp(chunk_identifier.data(), "CRNG") == 0) + { + Uint32 chunkLen; + CHECK_READ(libendian::be_read_ui(&chunkLen, fp)); + if(chunkLen >= 8) + { + uint16_t padding, rate, flags; + CHECK_READ(libendian::be_read_us(&padding, fp)); + CHECK_READ(libendian::be_read_us(&rate, fp)); + CHECK_READ(libendian::be_read_us(&flags, fp)); + uint8_t firstClr, lastClr; + CHECK_READ(libendian::read(&firstClr, 1, fp)); + CHECK_READ(libendian::read(&lastClr, 1, fp)); + // Only register valid animations: at least 2 colors and non-zero rate + if(rate > 0 && lastClr > firstClr) + { + PaletteAnimation anim; + anim.moveUp = (flags & 2) != 0; + if(rate) + anim.moveUp = true; + anim.rate = rate; + anim.firstClr = firstClr; + anim.lastClr = lastClr; + anim.currentOffset = 0; + anim.lastAppliedOffset = 0; + anim.lastUpdateTime = SDL_GetTicks(); + switch(tilesetSlot) + { + case TILESET_GREENLAND_8BPP: global::paletteAnimations[MAP_GREENLAND].push_back(anim); break; + case TILESET_WASTELAND_8BPP: global::paletteAnimations[MAP_WASTELAND].push_back(anim); break; + case TILESET_WINTERLAND_8BPP: global::paletteAnimations[MAP_WINTERLAND].push_back(anim); break; + default: break; + } + } + if(chunkLen > 8) + { + uint32_t remaining = chunkLen - 8; + if(remaining & 1) + remaining++; + fseek(fp, remaining, SEEK_CUR); + } else if(chunkLen & 1) + fseek(fp, 1, SEEK_CUR); + } else + { + if(chunkLen & 1) + chunkLen++; + fseek(fp, chunkLen, SEEK_CUR); + } + } else { Uint32 chunkLen; CHECK_READ(libendian::be_read_ui(&chunkLen, fp)); diff --git a/CMap.h b/CMap.h index fbb3352..7fdad53 100644 --- a/CMap.h +++ b/CMap.h @@ -150,6 +150,13 @@ class CMap render(); return Surf_Map.get(); } + /// Returns nullptr if the surface is not 8-bit or not created yet + SDL_Palette* getSurfacePalette() const + { + if(!Surf_Map || Surf_Map->format->BytesPerPixel != 1) + return nullptr; + return Surf_Map->format->palette; + } DisplayRectangle getDisplayRect() { return displayRect; } void setDisplayRect(const DisplayRectangle& displayRect) { this->displayRect = displayRect; } auto& getPlayerHQx() { return PlayerHQx; } diff --git a/CSurface.cpp b/CSurface.cpp index 730f3f4..74f0ed4 100644 --- a/CSurface.cpp +++ b/CSurface.cpp @@ -15,12 +15,15 @@ #include #include #include +#include // Disable SGE's internal surface locking once at startup; terrain drawing is // the only remaining consumer of SGE functions and the caller already handles // locking. Was originally called in CGame::Init(). static bool sgeLockOff = (sge_Lock_OFF(), true); +static unsigned tilesetIdxForMapType(MapType mapType, bool want32bit); + namespace { const TerrainDesc* getTerrainDesc(const bobMAP& map, Uint8 rawTextureId) { @@ -623,11 +626,10 @@ bool GetAdjustedPoints(const DisplayRectangle& displayRect, const bobMAP& myMap, } } // namespace -void CSurface::GetTerrainTextureCoords(MapType mapType, TriangleTerrainType texture, bool isRSU, int texture_move, - Point16& upper, Point16& left, Point16& right, Point16& upper2, Point16& left2, - Point16& right2) +void CSurface::GetTerrainTextureCoords(MapType mapType, TriangleTerrainType texture, bool isRSU, Point16& upper, + Point16& left, Point16& right, Point16& upper2, Point16& left2, Point16& right2) { - const auto animOffset = Point16(-texture_move, texture_move); + // Offset-shifting animation replaced by palette cycling animation (UpdatePaletteAnimations) switch(texture) { // in case of USD-Triangle "upper.x" and "upper.y" means "lowerX" and "lowerY" @@ -657,14 +659,14 @@ void CSurface::GetTerrainTextureCoords(MapType mapType, TriangleTerrainType text { if(isRSU) { - upper2 = Point16(231, 61) + animOffset; - left2 = Point16(207, 62) + animOffset; - right2 = Point16(223, 78) + animOffset; + upper2 = Point16(231, 61); + left2 = Point16(207, 62); + right2 = Point16(223, 78); } else { - upper2 = Point16(224, 79) + animOffset; - left2 = Point16(232, 62) + animOffset; - right2 = Point16(245, 76) + animOffset; + upper2 = Point16(224, 79); + left2 = Point16(232, 62); + right2 = Point16(245, 76); } } break; @@ -676,14 +678,14 @@ void CSurface::GetTerrainTextureCoords(MapType mapType, TriangleTerrainType text { if(isRSU) { - upper2 = Point16(231, 61) + animOffset; - left2 = Point16(207, 62) + animOffset; - right2 = Point16(223, 78) + animOffset; + upper2 = Point16(231, 61); + left2 = Point16(207, 62); + right2 = Point16(223, 78); } else { - upper2 = Point16(224, 79) + animOffset; - left2 = Point16(232, 62) + animOffset; - right2 = Point16(245, 76) + animOffset; + upper2 = Point16(224, 79); + left2 = Point16(232, 62); + right2 = Point16(245, 76); } } break; @@ -700,14 +702,14 @@ void CSurface::GetTerrainTextureCoords(MapType mapType, TriangleTerrainType text case TRIANGLE_TEXTURE_WATER__: if(isRSU) { - upper = Point16(231, 61) + animOffset; - left = Point16(207, 62) + animOffset; - right = Point16(223, 78) + animOffset; + upper = Point16(231, 61); + left = Point16(207, 62); + right = Point16(223, 78); } else { - upper = Point16(224, 79) + animOffset; - left = Point16(232, 62) + animOffset; - right = Point16(245, 76) + animOffset; + upper = Point16(224, 79); + left = Point16(232, 62); + right = Point16(245, 76); } break; case TRIANGLE_TEXTURE_MEADOW1: @@ -753,14 +755,14 @@ void CSurface::GetTerrainTextureCoords(MapType mapType, TriangleTerrainType text case TRIANGLE_TEXTURE_LAVA: if(isRSU) { - upper = Point16(231, 117) + animOffset; - left = Point16(207, 118) + animOffset; - right = Point16(223, 134) + animOffset; + upper = Point16(231, 117); + left = Point16(207, 118); + right = Point16(223, 134); } else { - upper = Point16(224, 135) + animOffset; - left = Point16(232, 118) + animOffset; - right = Point16(245, 132) + animOffset; + upper = Point16(224, 135); + left = Point16(232, 118); + right = Point16(245, 132); } break; case TRIANGLE_TEXTURE_MINING_MEADOW: @@ -787,16 +789,8 @@ void CSurface::DrawTriangle(SDL_Surface* display, const DisplayRectangle& displa return; // for moving water, lava, objects and so on - // This is very tricky: there are ice floes in the winterland and the water under this floes is moving. - // I don't know how this works in original settlers 2 but i solved it this way: - // i texture the triangle with normal water and then draw the floe over it. To Extract the floe - // from it's surrounded water, i use this color keys below. These are the color values for the water texture. - // I wrote a special SGE-Function that uses these color keys and ignores them in the Surf_Tileset. - static std::array colorkeys = {14191, 14195, 13167, 13159, 11119}; - static int texture_move = 0; static int roundCount = 0; static Uint32 roundTimeObjects = SDL_GetTicks(); - static Uint32 roundTimeTextures = SDL_GetTicks(); if(SDL_GetTicks() - roundTimeObjects > 30) { roundTimeObjects = SDL_GetTicks(); @@ -805,34 +799,10 @@ void CSurface::DrawTriangle(SDL_Surface* display, const DisplayRectangle& displa else roundCount++; } - if(SDL_GetTicks() - roundTimeTextures > 170) - { - roundTimeTextures = SDL_GetTicks(); - texture_move++; - if(texture_move > 14) - texture_move = 0; - } SDL_Surface* Surf_Tileset; - switch(type) - { - case MAP_GREENLAND: - default: - Surf_Tileset = global::bmpArray[global::s2->getMapObj()->getBitsPerPixel() == 8 ? TILESET_GREENLAND_8BPP : - TILESET_GREENLAND_32BPP] - .surface.get(); - break; - case MAP_WASTELAND: - Surf_Tileset = global::bmpArray[global::s2->getMapObj()->getBitsPerPixel() == 8 ? TILESET_WASTELAND_8BPP : - TILESET_WASTELAND_32BPP] - .surface.get(); - break; - case MAP_WINTERLAND: - Surf_Tileset = global::bmpArray[global::s2->getMapObj()->getBitsPerPixel() == 8 ? TILESET_WINTERLAND_8BPP : - TILESET_WINTERLAND_32BPP] - .surface.get(); - break; - } + const bool want32 = global::s2->getMapObj()->getBitsPerPixel() != 8; + Surf_Tileset = global::bmpArray[tilesetIdxForMapType(type, want32)].surface.get(); bool const isRSU = p1.y < p2.y; @@ -842,7 +812,7 @@ void CSurface::DrawTriangle(SDL_Surface* display, const DisplayRectangle& displa Point16 upper, left, right, upper2, left2, right2; auto const texture = TriangleTerrainType((isRSU ? P1.rsuTexture : P2.usdTexture) & ~0x40); // Mask out harbor bit - GetTerrainTextureCoords(type, texture, isRSU, texture_move, upper, left, right, upper2, left2, right2); + GetTerrainTextureCoords(type, texture, isRSU, upper, left, right, upper2, left2, right2); // draw the triangle // do not shade water and lava @@ -852,30 +822,13 @@ void CSurface::DrawTriangle(SDL_Surface* display, const DisplayRectangle& displa left.y, right.x, right.y); else { - // draw special winterland textures with moving water (ice floe textures) - if(type == MAP_WINTERLAND && (texture == TRIANGLE_TEXTURE_SNOW || texture == TRIANGLE_TEXTURE_SWAMP)) - { - sge_TexturedTrigon(display, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, Surf_Tileset, upper2.x, upper2.y, - left2.x, left2.y, right2.x, right2.y); - if(global::s2->getMapObj()->getBitsPerPixel() == 8) - sge_PreCalcFadedTexturedTrigonColorKeys(display, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, Surf_Tileset, - upper.x, upper.y, left.x, left.y, right.x, right.y, - P1.shading << 8, P2.shading << 8, P3.shading << 8, - gouData[type], colorkeys.data(), colorkeys.size()); - else - sge_FadedTexturedTrigonColorKeys(display, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, Surf_Tileset, upper.x, - upper.y, left.x, left.y, right.x, right.y, P1.i, P2.i, P3.i, - colorkeys.data(), colorkeys.size()); - } else - { - if(global::s2->getMapObj()->getBitsPerPixel() == 8) - sge_PreCalcFadedTexturedTrigon(display, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, Surf_Tileset, upper.x, - upper.y, left.x, left.y, right.x, right.y, P1.shading << 8, - P2.shading << 8, P3.shading << 8, gouData[type]); - else - sge_FadedTexturedTrigon(display, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, Surf_Tileset, upper.x, upper.y, - left.x, left.y, right.x, right.y, P1.i, P2.i, P3.i); - } + if(global::s2->getMapObj()->getBitsPerPixel() == 8) + sge_PreCalcFadedTexturedTrigon(display, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, Surf_Tileset, upper.x, + upper.y, left.x, left.y, right.x, right.y, P1.shading << 8, + P2.shading << 8, P3.shading << 8, gouData[type]); + else + sge_FadedTexturedTrigon(display, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, Surf_Tileset, upper.x, upper.y, + left.x, left.y, right.x, right.y, P1.i, P2.i, P3.i); } return; } @@ -1534,3 +1487,96 @@ float CSurface::absf(float a) else return a * (-1); } + +/// Map a MapType to the 8-bit or 32-bit tileset bmpArray slot +static unsigned tilesetIdxForMapType(MapType mapType, bool want32bit) +{ + switch(mapType) + { + case MAP_GREENLAND: + default: return want32bit ? TILESET_GREENLAND_32BPP : TILESET_GREENLAND_8BPP; + case MAP_WASTELAND: return want32bit ? TILESET_WASTELAND_32BPP : TILESET_WASTELAND_8BPP; + case MAP_WINTERLAND: return want32bit ? TILESET_WINTERLAND_32BPP : TILESET_WINTERLAND_8BPP; + } +} + +/// Apply a delta rotation to a single palette range +static void rotatePaletteRange(SDL_Palette* pal, uint8_t firstClr, int colorCount, int deltaOffset, bool moveUp) +{ + std::array rotated; + memcpy(rotated.data(), pal->colors, sizeof(SDL_Color) * 256); + for(int i = 0; i < colorCount; i++) + { + int srcIdx = moveUp ? (i - deltaOffset + colorCount) % colorCount : (i + deltaOffset) % colorCount; + rotated[firstClr + i] = pal->colors[firstClr + srcIdx]; + } + SDL_SetPaletteColors(pal, rotated.data() + firstClr, firstClr, colorCount); +} + +void CSurface::UpdatePaletteAnimations(MapType mapType) +{ + if(global::paletteAnimations[mapType].empty()) + return; + + const auto tilesetIdx8 = tilesetIdxForMapType(mapType, false); + auto* surf8 = global::bmpArray[tilesetIdx8].surface.get(); + if(!surf8 || !surf8->format->palette) + return; + + SDL_Palette* mapPal = + (global::s2 && global::s2->getMapObj()) ? global::s2->getMapObj()->getSurfacePalette() : nullptr; + + // 32-bit tileset surface for blitting after palette updates + const auto tilesetIdx32 = tilesetIdxForMapType(mapType, true); + auto& surf32 = *global::bmpArray[tilesetIdx32].surface; + + const unsigned now = SDL_GetTicks(); + bool anyUpdate = false; + + for(auto& anim : global::paletteAnimations[mapType]) + { + if(anim.rate == 0) + continue; + + const int colorCount = anim.lastClr - anim.firstClr + 1; + if(colorCount <= 1) + continue; + + // Time per step: (8192/30) / rate seconds, converted to ms + const float intervalMs = (8192.0f / 30.0f) * 1000.0f / anim.rate; + + const unsigned elapsed = now - anim.lastUpdateTime; + if(elapsed < static_cast(intervalMs)) + continue; + + int steps = static_cast(elapsed / intervalMs); + anim.lastUpdateTime += static_cast(steps * intervalMs); + + int newOffset = anim.moveUp ? (anim.currentOffset + steps) % colorCount : + (anim.currentOffset - steps + colorCount * steps) % colorCount; + int deltaOffset = (newOffset - anim.lastAppliedOffset + colorCount) % colorCount; + if(deltaOffset == 0) + continue; + anim.lastAppliedOffset = newOffset; + anim.currentOffset = newOffset; + + rotatePaletteRange(surf8->format->palette, anim.firstClr, colorCount, deltaOffset, anim.moveUp); + if(mapPal) + rotatePaletteRange(mapPal, anim.firstClr, colorCount, deltaOffset, anim.moveUp); + anyUpdate = true; + } + + if(!anyUpdate) + return; + + // Blit 8-bit tileset to 32-bit so the rotated palette takes effect in 32bpp mode + if(SDL_MUSTLOCK(&surf32)) + SDL_LockSurface(&surf32); + if(SDL_MUSTLOCK(surf8)) + SDL_LockSurface(surf8); + SDL_BlitSurface(surf8, nullptr, &surf32, nullptr); + if(SDL_MUSTLOCK(surf8)) + SDL_UnlockSurface(surf8); + if(SDL_MUSTLOCK(&surf32)) + SDL_UnlockSurface(&surf32); +} diff --git a/CSurface.h b/CSurface.h index 3677579..072f2b2 100644 --- a/CSurface.h +++ b/CSurface.h @@ -59,6 +59,8 @@ class CSurface static void get_nodeVectors(bobMAP& myMap); static void update_shading(bobMAP& myMap, Position pos); + /// Update palette cycling animations for the given map type's tileset + static void UpdatePaletteAnimations(MapType mapType); private: // to decide what to draw, triangle-textures or objects and texture-borders @@ -73,7 +75,7 @@ class CSurface static void update_flatVectors(bobMAP& myMap, Position pos); // update nodeVector based on new flatVectors around it static void update_nodeVector(bobMAP& myMap, Position pos); - static void GetTerrainTextureCoords(MapType mapType, TriangleTerrainType texture, bool isRSU, int texture_move, - Point16& upper, Point16& left, Point16& right, Point16& upper2, Point16& left2, + static void GetTerrainTextureCoords(MapType mapType, TriangleTerrainType texture, bool isRSU, Point16& upper, + Point16& left, Point16& right, Point16& upper2, Point16& left2, Point16& right2); }; diff --git a/globals.cpp b/globals.cpp index da4edcf..2000b73 100644 --- a/globals.cpp +++ b/globals.cpp @@ -12,6 +12,8 @@ std::vector global::bmpArray(MAXBOBBMP); std::vector global::shadowArray(MAXBOBSHADOW); // array for all palettes std::vector global::palArray(MAXBOBPAL); +// Palette animations per tileset, indexed by MapType (0=Greenland, 1=Wasteland, 2=Winterland). +std::array, 3> global::paletteAnimations; // the game object CGame* global::s2; diff --git a/globals.h b/globals.h index 5b4dbaf..695cadb 100644 --- a/globals.h +++ b/globals.h @@ -8,12 +8,14 @@ #include "gameData/WorldDescription.h" #include #include +#include #include class CGame; struct bobBMP; struct bobSHADOW; struct bobPAL; +struct PaletteAnimation; namespace global { // array for all pictures @@ -22,6 +24,9 @@ extern std::vector bmpArray; extern std::vector shadowArray; // array for all palettes extern std::vector palArray; +// Palette animations per tileset, indexed by MapType (0=Greenland, 1=Wasteland, 2=Winterland). +// Each entry is a list of CRNG chunk animations applied to the whole tileset palette. +extern std::array, 3> paletteAnimations; // the game object extern CGame* s2; // Path to game data (must not be empty!) diff --git a/include/defines.h b/include/defines.h index 5adf11c..b6e571e 100644 --- a/include/defines.h +++ b/include/defines.h @@ -91,6 +91,18 @@ struct bobPAL std::array colors; }; +// Palette animation parsed from a CRNG chunk in an LBM file +struct PaletteAnimation +{ + bool moveUp; + uint16_t rate; + uint8_t firstClr; + uint8_t lastClr; + int currentOffset; + int lastAppliedOffset; + unsigned lastUpdateTime; +}; + // Structure for Bobtype 7 (Shadow-Bitmaps) struct bobSHADOW {