diff --git a/src/Application/Startup/GameStarter.cpp b/src/Application/Startup/GameStarter.cpp index 3ae485718b50..cf78edb7a54d 100644 --- a/src/Application/Startup/GameStarter.cpp +++ b/src/Application/Startup/GameStarter.cpp @@ -183,6 +183,9 @@ void GameStarter::initWithLogger() { GameStarter::~GameStarter() { _application->removeComponent(); // Join the control thread first. + _game.reset(); + _engine.reset(); + ::engine = nullptr; ::render = nullptr; ::application = nullptr; diff --git a/src/Engine/CMakeLists.txt b/src/Engine/CMakeLists.txt index 591424e88fb6..3f5720c5a185 100644 --- a/src/Engine/CMakeLists.txt +++ b/src/Engine/CMakeLists.txt @@ -24,7 +24,8 @@ set(ENGINE_SOURCES TeleportPoint.cpp GameResourceManager.cpp mm7_data.cpp - mm7text_ru.cpp) + mm7text_ru.cpp + Seasons.cpp) set(ENGINE_HEADERS ArenaEnumFunctions.h @@ -54,7 +55,7 @@ set(ENGINE_HEADERS GameResourceManager.h mm7_data.h stru314.h - Data/AutonoteData.h) + Seasons.h) add_library(engine STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS}) target_check_style(engine) diff --git a/src/Engine/Data/CMakeLists.txt b/src/Engine/Data/CMakeLists.txt index 11bcf5059bae..81f8cec97efd 100644 --- a/src/Engine/Data/CMakeLists.txt +++ b/src/Engine/Data/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.27 FATAL_ERROR) -set(ENGINE_DATA_SOURCES) +set(ENGINE_DATA_SOURCES + TileEnumFunctions.cpp) set(ENGINE_DATA_HEADERS AutonoteEnums.h @@ -15,8 +16,9 @@ set(ENGINE_DATA_HEADERS IconFrameData.h PortraitFrameData.h TileData.h - TileEnums.h) + TileEnums.h + TileEnumFunctions.h) -add_library(engine_data INTERFACE ${ENGINE_DATA_SOURCES} ${ENGINE_DATA_HEADERS}) -target_link_libraries(engine_data INTERFACE library_serialization utility) +add_library(engine_data STATIC ${ENGINE_DATA_SOURCES} ${ENGINE_DATA_HEADERS}) +target_link_libraries(engine_data PUBLIC library_serialization utility) target_check_style(engine_data) diff --git a/src/Engine/Data/TileData.h b/src/Engine/Data/TileData.h index ec1d712dcbd8..0f0dba281b3e 100644 --- a/src/Engine/Data/TileData.h +++ b/src/Engine/Data/TileData.h @@ -8,7 +8,7 @@ struct TileData { std::string name; uint16_t uTileID = 0; - TileSet tileset = TILE_SET_INVALID; + Tileset tileset = TILESET_INVALID; TileVariant uSection = TILE_VARIANT_BASE1; TileFlags uAttributes; }; diff --git a/src/Engine/Data/TileEnumFunctions.cpp b/src/Engine/Data/TileEnumFunctions.cpp new file mode 100644 index 000000000000..c41c5b901ad8 --- /dev/null +++ b/src/Engine/Data/TileEnumFunctions.cpp @@ -0,0 +1,67 @@ +#include "TileEnumFunctions.h" + +#include + +SoundId walkSoundForTileset(Tileset tileset, bool isRunning) { + switch (tileset) { + default: + assert(false); + [[fallthrough]]; + case TILESET_INVALID: + return isRunning ? SOUND_RunDirt : SOUND_WalkDirt; + case TILESET_GRASS: + return isRunning ? SOUND_RunGrass : SOUND_WalkGrass; + case TILESET_SNOW: + return isRunning ? SOUND_RunSnow : SOUND_WalkSnow; + case TILESET_DESERT: + return isRunning ? SOUND_RunDesert : SOUND_WalkDesert; + case TILESET_COOLED_LAVA: + return isRunning ? SOUND_RunCooledLava : SOUND_WalkCooledLava; + case TILESET_DIRT: + return isRunning ? SOUND_RunDirt : SOUND_WalkDirt; // Water sounds were used. + case TILESET_WATER: + return isRunning ? SOUND_RunWater : SOUND_WalkWater; // Dirt sounds were used. + case TILESET_BADLANDS: + return isRunning ? SOUND_RunBadlands : SOUND_WalkBadlands; + case TILESET_SWAMP: + return isRunning ? SOUND_RunSwamp : SOUND_WalkSwamp; + case TILESET_TROPICAL: + return isRunning ? SOUND_RunGrass : SOUND_WalkGrass; // TODO(Nik-RE-dev): is that correct? + case TILESET_CITY: + return isRunning ? SOUND_RunGround : SOUND_WalkGround; // TODO(Nik-RE-dev): is that correct? + case TILESET_ROAD_GRASS_COBBLE: + case TILESET_ROAD_GRASS_DIRT: + case TILESET_ROAD_SNOW_COBBLE: + case TILESET_ROAD_SNOW_DIRT: + case TILESET_ROAD_SAND_COBBLE: + case TILESET_ROAD_SAND_DIRT: + case TILESET_ROAD_VOLCANO_COBBLE: + case TILESET_ROAD_VOLCANO_DIRT: + case TILESET_ROAD_CRACKED_COBBLE: + case TILESET_ROAD_CRACKED_DIRT: + case TILESET_ROAD_SWAMP_COBBLE: + case TILESET_ROAD_SWAMP_DIRT: + case TILESET_ROAD_TROPICAL_COBBLE: + case TILESET_ROAD_TROPICAL_DIRT: + return isRunning ? SOUND_RunRoad : SOUND_WalkRoad; + case TILESET_ROAD_CITY_STONE: + return isRunning ? SOUND_RunGround : SOUND_WalkGround; // TODO(Nik-RE-dev): is that correct? + } +} + +int foodRequiredForTileset(Tileset tileset) { + switch (tileset) { + case TILESET_GRASS: + return 1; + case TILESET_SNOW: + case TILESET_SWAMP: + return 3; + case TILESET_COOLED_LAVA: + case TILESET_BADLANDS: + return 4; + case TILESET_DESERT: + return 5; + default: + return 2; + } +} diff --git a/src/Engine/Data/TileEnumFunctions.h b/src/Engine/Data/TileEnumFunctions.h new file mode 100644 index 000000000000..4e6382c6826e --- /dev/null +++ b/src/Engine/Data/TileEnumFunctions.h @@ -0,0 +1,19 @@ +#include "TileEnums.h" + +#include "Media/Audio/SoundEnums.h" + +#include "Utility/Segment.h" + +inline Segment allSpecialTileVariants() { + return {TILE_VARIANT_FIRST_SPECIAL, TILE_VARIANT_LAST_SPECIAL}; +} + +/** + * @param tileset Tileset to get walk/run sound for. + * @param isRunning Run flag. + * @return Sound id to use. + * @offset 0x47EE49 + */ +SoundId walkSoundForTileset(Tileset tileset, bool isRunning); + +int foodRequiredForTileset(Tileset tileset); diff --git a/src/Engine/Data/TileEnums.h b/src/Engine/Data/TileEnums.h index 52673625a3f5..451575b414b4 100644 --- a/src/Engine/Data/TileEnums.h +++ b/src/Engine/Data/TileEnums.h @@ -1,7 +1,6 @@ #pragma once #include "Utility/Flags.h" -#include "Utility/Segment.h" enum class TileFlag { TILE_BURN = 0x1, @@ -62,41 +61,37 @@ enum class TileVariant { }; using enum TileVariant; -inline Segment allSpecialTileSects() { - return {TILE_VARIANT_FIRST_SPECIAL, TILE_VARIANT_LAST_SPECIAL}; -} - /** * Tile set id. * * Most of these tile sets don't exist in mm7 data, see comments. */ -enum class TileSet { - TILE_SET_INVALID = 255, // Tile with id 0 has tile set = 255. - TILE_SET_GRASS = 0, - TILE_SET_SNOW = 1, - TILE_SET_DESERT = 2, // Sand. - TILE_SET_COOLED_LAVA = 3, // Somehow this tileset is all dirt in the data files. - TILE_SET_DIRT = 4, // This one has only 3 tiles. - TILE_SET_WATER = 5, // Water tile & shoreline tiles. - TILE_SET_BADLANDS = 6, // Looks like Deyja. - TILE_SET_SWAMP = 7, - TILE_SET_TROPICAL = 8, // This is all dirt. - TILE_SET_CITY = 9, // This is sand too, lol. - TILE_SET_ROAD_GRASS_COBBLE = 10, // Cobble road on dirt actually. - TILE_SET_ROAD_GRASS_DIRT = 11, // This is all dirt. - TILE_SET_ROAD_SNOW_COBBLE = 12, // This is all dirt. - TILE_SET_ROAD_SNOW_DIRT = 13, // This is all dirt. - TILE_SET_ROAD_SAND_COBBLE = 14, // This doesn't exist in mm7 tiles at all. - TILE_SET_ROAD_SAND_DIRT = 15, // This doesn't exist in mm7 tiles at all. - TILE_SET_ROAD_VOLCANO_COBBLE = 16, // This is all dirt. - TILE_SET_ROAD_VOLCANO_DIRT = 17, // This is all dirt. - TILE_SET_ROAD_CRACKED_COBBLE = 22, // This is all dirt. - TILE_SET_ROAD_CRACKED_DIRT = 23, // This is all dirt. - TILE_SET_ROAD_SWAMP_COBBLE = 24, // This is all dirt. - TILE_SET_ROAD_SWAMP_DIRT = 25, // This is all dirt. - TILE_SET_ROAD_TROPICAL_COBBLE = 26, // This is all dirt. - TILE_SET_ROAD_TROPICAL_DIRT = 27, // This is all dirt. - TILE_SET_ROAD_CITY_STONE = 28, // This is all dirt. +enum class Tileset { + TILESET_INVALID = 255, // Tile with id 0 has tile set = 255. + TILESET_GRASS = 0, + TILESET_SNOW = 1, + TILESET_DESERT = 2, // Sand. + TILESET_COOLED_LAVA = 3, // Somehow this tileset is all dirt in the data files. + TILESET_DIRT = 4, // This one has only 3 tiles. + TILESET_WATER = 5, // Water tile & shoreline tiles. + TILESET_BADLANDS = 6, // Looks like Deyja. + TILESET_SWAMP = 7, + TILESET_TROPICAL = 8, // This is all dirt. + TILESET_CITY = 9, // This is sand too, lol. + TILESET_ROAD_GRASS_COBBLE = 10, // Cobble road on dirt actually. + TILESET_ROAD_GRASS_DIRT = 11, // This is all dirt. + TILESET_ROAD_SNOW_COBBLE = 12, // This is all dirt. + TILESET_ROAD_SNOW_DIRT = 13, // This is all dirt. + TILESET_ROAD_SAND_COBBLE = 14, // This doesn't exist in mm7 tiles at all. + TILESET_ROAD_SAND_DIRT = 15, // This doesn't exist in mm7 tiles at all. + TILESET_ROAD_VOLCANO_COBBLE = 16, // This is all dirt. + TILESET_ROAD_VOLCANO_DIRT = 17, // This is all dirt. + TILESET_ROAD_CRACKED_COBBLE = 22, // This is all dirt. + TILESET_ROAD_CRACKED_DIRT = 23, // This is all dirt. + TILESET_ROAD_SWAMP_COBBLE = 24, // This is all dirt. + TILESET_ROAD_SWAMP_DIRT = 25, // This is all dirt. + TILESET_ROAD_TROPICAL_COBBLE = 26, // This is all dirt. + TILESET_ROAD_TROPICAL_DIRT = 27, // This is all dirt. + TILESET_ROAD_CITY_STONE = 28, // This is all dirt. }; -using enum TileSet; +using enum Tileset; diff --git a/src/Engine/Graphics/Camera.cpp b/src/Engine/Graphics/Camera.cpp index ca18b042a6b8..a87b03905cd7 100644 --- a/src/Engine/Graphics/Camera.cpp +++ b/src/Engine/Graphics/Camera.cpp @@ -518,27 +518,6 @@ void Camera3D::CalculateRotations(int cameraYaw, int cameraPitch) { _pitchRotationCosine = std::cos((pi_double + pi_double) * _viewPitch / 2048.0); } -//----- (00436A6D) -------------------------------------------------------- -float Camera3D::GetPolygonMinZ(RenderVertexSoft *pVertices, unsigned int uStripType) { - float result = FLT_MAX; - for (unsigned i = 0; i < uStripType; i++) { - if (pVertices[i].vWorldPosition.z < result) { - result = pVertices[i].vWorldPosition.z; - } - } - return result; -} - -//----- (00436A40) -------------------------------------------------------- -float Camera3D::GetPolygonMaxZ(RenderVertexSoft *pVertex, unsigned int uStripType) { - float result = FLT_MIN; - for (unsigned i = 0; i < uStripType; i++) { - if (pVertex[i].vWorldPosition.z > result) - result = pVertex[i].vWorldPosition.z; - } - return result; -} - void Camera3D::CullByNearClip(RenderVertexSoft *pverts, unsigned *unumverts) { float near = GetNearClip(); diff --git a/src/Engine/Graphics/Camera.h b/src/Engine/Graphics/Camera.h index dad428e119e1..20b0e91e26ae 100644 --- a/src/Engine/Graphics/Camera.h +++ b/src/Engine/Graphics/Camera.h @@ -27,11 +27,6 @@ struct Camera3D { RenderVertexSoft *pVertices, signed int NumFrustumPlanes); - float GetPolygonMaxZ(RenderVertexSoft *pVertex, - unsigned int uStripType); - float GetPolygonMinZ(RenderVertexSoft *pVertices, - unsigned int uStripType); - void LightmapNeerClip(RenderVertexSoft *pInVertices, int uNumInVertices, RenderVertexSoft *pOutVertices, diff --git a/src/Engine/Graphics/Collisions.cpp b/src/Engine/Graphics/Collisions.cpp index 1a49ea6ec164..9e78f38b0f0d 100644 --- a/src/Engine/Graphics/Collisions.cpp +++ b/src/Engine/Graphics/Collisions.cpp @@ -744,7 +744,7 @@ void ProcessActorCollisionsODM(Actor &actor, bool isFlying) { break; CollideOutdoorWithModels(true); - CollideOutdoorWithDecorations(WorldPosToGrid(actor.pos)); + CollideOutdoorWithDecorations(worldToGrid(actor.pos)); CollideWithParty(false); _46ED8A_collide_against_sprite_objects(Pid(OBJECT_Actor, actor.id)); @@ -1023,7 +1023,7 @@ void ProcessPartyCollisionsODM(Vec3f *partyNewPos, Vec3f *partyInputSpeed, int * } CollideOutdoorWithModels(true); - CollideOutdoorWithDecorations(WorldPosToGrid(pParty->pos)); + CollideOutdoorWithDecorations(worldToGrid(pParty->pos)); _46ED8A_collide_against_sprite_objects(Pid::character(0)); if (!engine->config->gameplay.NoPartyActorCollisions.value()) { for (size_t actor_id = 0; actor_id < pActors.size(); ++actor_id) diff --git a/src/Engine/Graphics/DecalBuilder.cpp b/src/Engine/Graphics/DecalBuilder.cpp index 8fae081fad4d..78c8db404fa0 100644 --- a/src/Engine/Graphics/DecalBuilder.cpp +++ b/src/Engine/Graphics/DecalBuilder.cpp @@ -230,7 +230,7 @@ bool DecalBuilder::ApplyBloodSplat_OutdoorFace(ODMFace *pFace) { //----- (0049BE8A) -------------------------------------------------------- // apply outdoor blodsplats - check to see if bloodsplat hits terrain triangle -bool DecalBuilder::ApplyBloodSplatToTerrain(bool fading, Vec3f *terrnorm, float *tridotdist, +bool DecalBuilder::ApplyBloodSplatToTerrain(bool fading, const Vec3f &terrnorm, float *tridotdist, RenderVertexSoft *triverts, const int whichsplat) { // tracks how many decals are applied to this tri this->uNumSplatsThisFace = 0; @@ -240,8 +240,8 @@ bool DecalBuilder::ApplyBloodSplatToTerrain(bool fading, Vec3f *terrnorm, float if (NumBloodsplats > 0) { // check plane distance - *tridotdist = -dot(triverts->vWorldPosition, *terrnorm); - float planedist = dot(*terrnorm, bloodsplat_container->pBloodsplats_to_apply[whichsplat].pos) + *tridotdist + 0.5f; + *tridotdist = -dot(triverts->vWorldPosition, terrnorm); + float planedist = dot(terrnorm, bloodsplat_container->pBloodsplats_to_apply[whichsplat].pos) + *tridotdist + 0.5f; if (planedist <= bloodsplat_container->pBloodsplats_to_apply[whichsplat].radius) { // blood splat hits this terrain tri diff --git a/src/Engine/Graphics/DecalBuilder.h b/src/Engine/Graphics/DecalBuilder.h index 848f79ae357d..bb7426fe819c 100644 --- a/src/Engine/Graphics/DecalBuilder.h +++ b/src/Engine/Graphics/DecalBuilder.h @@ -101,7 +101,7 @@ struct DecalBuilder { * * @return True if bloodsplat_container->uNumBloodsplats > 0, false otherwise. */ - bool ApplyBloodSplatToTerrain(bool fading, Vec3f *terrnorm, float *tridotdist, + bool ApplyBloodSplatToTerrain(bool fading, const Vec3f &terrnorm, float *tridotdist, RenderVertexSoft *triverts, const int whichsplat); void DrawDecals(float z_bias); void DrawBloodsplats(); diff --git a/src/Engine/Graphics/Outdoor.cpp b/src/Engine/Graphics/Outdoor.cpp index 8f6c16534b58..4a6aef9c6f6a 100644 --- a/src/Engine/Graphics/Outdoor.cpp +++ b/src/Engine/Graphics/Outdoor.cpp @@ -38,6 +38,8 @@ #include "Engine/Graphics/BspRenderer.h" #include "Engine/MapInfo.h" #include "Engine/LOD.h" +#include "Engine/Seasons.h" +#include "Engine/Data/TileEnumFunctions.h" #include "GUI/GUIProgressBar.h" #include "GUI/GUIWindow.h" @@ -188,7 +190,7 @@ void OutdoorLocation::ExecDraw(unsigned int bRedraw) { engine->StackPartyTorchLight(); // engine->PrepareBloodsplats(); // not used? - UpdateDiscoveredArea(WorldPosToGrid(pParty->pos)); + UpdateDiscoveredArea(worldToGrid(pParty->pos)); uNumDecorationsDrawnThisFrame = 0; uNumSpritesDrawnThisFrame = 0; @@ -249,11 +251,6 @@ double OutdoorLocation::GetFogDensityByTime() { } } -TileData *OutdoorLocation::getTileDescByPos(const Vec3f &pos) { - Vec2i gridPos = WorldPosToGrid(pos); - return getTileDescByGrid(gridPos.x, gridPos.y); -} - //----- (00488F5C) -------------------------------------------------------- bool OutdoorLocation::Initialize(std::string_view filename, int days_played, int respawn_interval_days, @@ -367,20 +364,7 @@ int OutdoorLocation::getNumFoodRequiredToRestInCurrentPos(const Vec3f &pos) { return 2; } - switch (getTileDescByPos(pos)->tileset) { - case TILE_SET_GRASS: - return 1; - case TILE_SET_SNOW: - case TILE_SET_SWAMP: - return 3; - case TILE_SET_COOLED_LAVA: - case TILE_SET_BADLANDS: - return 4; - case TILE_SET_DESERT: - return 5; - default: - return 2; - } + return foodRequiredForTileset(pTerrain.tilesetByPos(pos)); } //----- (00489487) -------------------------------------------------------- @@ -429,7 +413,7 @@ void OutdoorLocation::CreateDebugLocation() { this->location_filename = "i6.odm"; this->location_file_description = "MM6 Outdoor v1.00"; - this->pTerrain.CreateDebugTerrain(); + this->pTerrain.createDebugTerrain(); this->pSpawnPoints.clear(); this->pOMAP.fill(0); @@ -564,54 +548,9 @@ void OutdoorLocation::Load(std::string_view filename, int days_played, int respa } this->sky_texture = assets->getBitmap(loc_time.sky_texture_name); -} - -TileData *OutdoorLocation::getTileDescByGrid(int sX, int sY) { - int tileId = pTerrain.tileIdByGrid(Vec2i(sX, sY)); - - if (engine->config->graphics.SeasonsChange.value()) { - switch (pParty->uCurrentMonth) { - case 11: - case 0: - case 1: // winter - if (tileId >= 90) { // Tileset_Grass begins at TileID = 90 - if (tileId <= 95) // some grastyl entries - tileId = 348; - else if (tileId <= 113) // rest of grastyl & all grdrt* - tileId = 348 + (tileId - 96); - } - /*switch (v3) - { - case 102: v3 = 354; break; // grdrtNE -> SNdrtne - case 104: v3 = 356; break; // grdrtNW -> SNdrtnw - case 108: v3 = 360; break; // grdrtN -> SNdrtn - }*/ - break; - - case 2: - case 3: - case 4: // spring - case 8: - case 9: - case 10: // autumn - if (tileId >= 90 && - tileId <= 113) // just convert all Tileset_Grass to dirt - tileId = 1; - break; - - case 5: - case 6: - case 7: // summer - // all tiles are green grass by default - break; - default: - assert(pParty->uCurrentMonth >= 0 && - pParty->uCurrentMonth < 12); - } - } - - return &pTileTable->tiles[tileId]; + if (engine->config->graphics.SeasonsChange.value()) + pOutdoor->pTerrain.changeSeason(pParty->uCurrentMonth); } //----- (0047EF60) -------------------------------------------------------- @@ -1047,6 +986,15 @@ OutdoorLocation::OutdoorLocation() { this->sky_texture = nullptr; uLastSunlightUpdateMinute = 0; + + engine->config->graphics.SeasonsChange.addListener(this, [this](bool seasonsChange) { + pTerrain.changeSeason(seasonsChange ? pParty->uCurrentMonth : 6); + render->ReleaseTerrain(); + }); +} + +OutdoorLocation::~OutdoorLocation() { + engine->config->graphics.SeasonsChange.removeListeners(this); } // TODO(pskelton): Magic numbers @@ -1466,8 +1414,8 @@ void ODM_ProcessPartyActions() { pParty->setAirborne(true); Vec3f partyOldPosition = pParty->pos; - Vec2i partyOldGridPos = WorldPosToGrid(pParty->pos); - Vec2i partyNewGridPos = WorldPosToGrid(partyNewPos); + Vec2i partyOldGridPos = worldToGrid(pParty->pos); + Vec2i partyNewGridPos = worldToGrid(partyNewPos); // this gets if tile is not water bool partyCurrentOnLand = !pOutdoor->pTerrain.isWaterByGrid(partyOldGridPos); @@ -1611,12 +1559,11 @@ void ODM_ProcessPartyActions() { bool isModelWalk = !partyNotOnModel && pOutdoor->pBModels[modelId].pFaces[faceId].Visible(); SoundId sound = SOUND_Invalid; if (partyIsRunning) { - if (walkDelta >= 4 ) { + if (walkDelta >= 4) { if (isModelWalk) { sound = SOUND_RunWood; } else { - // Old comment: 56 is ground run - sound = pOutdoor->pTerrain.soundIdByGrid(WorldPosToGrid(partyOldPosition), true); + sound = walkSoundForTileset(pOutdoor->pTerrain.tilesetByPos(partyOldPosition), true); } } } else if (partyIsWalking) { @@ -1624,7 +1571,7 @@ void ODM_ProcessPartyActions() { if (isModelWalk) { sound = SOUND_RunWood; } else { - sound = pOutdoor->pTerrain.soundIdByGrid(WorldPosToGrid(partyOldPosition), false); + sound = walkSoundForTileset(pOutdoor->pTerrain.tilesetByPos(partyOldPosition), false); } } } @@ -1854,17 +1801,16 @@ void UpdateActors_ODM() { if (!uIsFlying && !tile1IsLand && !uIsAboveFloor && Actor_On_Terrain) { // on water and shouldnt be bool tileTestLand = false; // reset land found - Vec2i gridPos = WorldPosToGrid(pActors[Actor_ITR].pos); + Vec2i gridPos = worldToGrid(pActors[Actor_ITR].pos); for (int i = gridPos.x - 1; i <= gridPos.x + 1; i++) { // scan surrounding cells for land for (int j = gridPos.y - 1; j <= gridPos.y + 1; j++) { tileTestLand = !pOutdoor->pTerrain.isWaterByGrid({i, j}); if (tileTestLand) { // found land - int target_x = GridCellToWorldPosX(i); - int target_y = GridCellToWorldPosY(j); + Vec2i target = gridToWorld({i, j}); if (pActors[Actor_ITR].CanAct()) { // head to land - pActors[Actor_ITR].yawAngle = TrigLUT.atan2(target_x - pActors[Actor_ITR].pos.x, - target_y - pActors[Actor_ITR].pos.y); + pActors[Actor_ITR].yawAngle = TrigLUT.atan2(target.x - pActors[Actor_ITR].pos.x, + target.y - pActors[Actor_ITR].pos.y); pActors[Actor_ITR].currentActionTime = 0_ticks; pActors[Actor_ITR].currentActionLength = 128_ticks; pActors[Actor_ITR].aiState = Fleeing; @@ -2053,24 +1999,6 @@ int sub_47C3D7_get_fog_specular(int unused, int isSky, float screen_depth) { return (255 - v7) << 24; } -//----- (0047F44B) -------------------------------------------------------- -//----- (0047F458) -------------------------------------------------------- -Vec2i WorldPosToGrid(Vec3f worldPos) { - int worldX = worldPos.x; - int worldY = worldPos.y; - - // sar is in original exe, resulting -880 / 512 = -1 and -880 sar 9 = -2. - int gridX = (worldX >> 9) + 64; - int gridY = 63 - (worldY >> 9); - return Vec2i(gridX, gridY); -} - -//----- (0047F469) -------------------------------------------------------- -int GridCellToWorldPosX(int a1) { return (a1 - 64) << 9; } - -//----- (0047F476) -------------------------------------------------------- -int GridCellToWorldPosY(int a1) { return (64 - a1) << 9; } - //----- (00436A6D) -------------------------------------------------------- double OutdoorLocation::GetPolygonMinZ(RenderVertexSoft *pVertices, unsigned int unumverts) { double result = FLT_MAX; diff --git a/src/Engine/Graphics/Outdoor.h b/src/Engine/Graphics/Outdoor.h index 92f1f1a002cb..3b24cc1ff97b 100644 --- a/src/Engine/Graphics/Outdoor.h +++ b/src/Engine/Graphics/Outdoor.h @@ -29,6 +29,7 @@ struct DMap { struct OutdoorLocation { OutdoorLocation(); + ~OutdoorLocation(); // int New_SKY_NIGHT_ID; void ExecDraw(unsigned int bRedraw); void PrepareActorsDrawList(); @@ -44,15 +45,6 @@ struct OutdoorLocation { bool InitalizeActors(MapId a1); double GetFogDensityByTime(); - /** - * @offset 0x488EEF - */ - TileData *getTileDescByPos(const Vec3f &pos); - - /** - * @offset 0x47ED08 - */ - TileData *getTileDescByGrid(int uX, int uZ); bool Initialize(std::string_view filename, int days_played, int respawn_interval_days, bool * outdoors_was_respawned); @@ -126,10 +118,7 @@ void SetUnderwaterFog(); void loadAndPrepareODM(MapId mapid, bool bLoading); Color GetLevelFogColor(); int sub_47C3D7_get_fog_specular(int unused, int a2, float a3); -Vec2i WorldPosToGrid(Vec3f worldPos); -int GridCellToWorldPosX(int); -int GridCellToWorldPosY(int); void sub_481ED9_MessWithODMRenderParams(); void TeleportToStartingPoint(MapStartPoint point); // idb diff --git a/src/Engine/Graphics/OutdoorTerrain.cpp b/src/Engine/Graphics/OutdoorTerrain.cpp index 481d3b52b755..aa5bd1df4ff7 100644 --- a/src/Engine/Graphics/OutdoorTerrain.cpp +++ b/src/Engine/Graphics/OutdoorTerrain.cpp @@ -1,44 +1,78 @@ #include "OutdoorTerrain.h" +#include #include #include "Engine/Tables/TileTable.h" +#include "Engine/Snapshots/CompositeSnapshots.h" +#include "Engine/Snapshots/EntitySnapshots.h" +#include "Engine/Seasons.h" + +#include "Library/Snapshots/CommonSnapshots.h" #include "Outdoor.h" -//----- (0047CCE2) -------------------------------------------------------- -bool OutdoorTerrain::ZeroLandscape() { - this->pHeightmap.fill(0); - this->pTilemap.fill(90); - this->pAttributemap.fill(0); - return true; +static int mapToGlobalTileId(const std::array &baseIds, int localTileId) { + // Tiles in tilemap: + // [0..90) are mapped as-is, but seem to be mostly invalid. Only global tile ids [1..12] are valid (all are dirt), + // the rest are "pending", effectively invalid. + // [90..126) map to tileset #1. + // [126..162) map to tileset #2. + // [162..198) map to tileset #3. + // [198..234) map to tileset #4 (road). + // [234..255) are invalid. + if (localTileId < 90) + return localTileId; + + if (localTileId >= 234) + return 0; + + int tilesetIndex = (localTileId - 90) / 36; + int tilesetOffset = (localTileId - 90) % 36; + return baseIds[tilesetIndex] + tilesetOffset; } -//----- (0047F420) -------------------------------------------------------- -void OutdoorTerrain::LoadBaseTileIds() { - for (unsigned i = 0; i < 3; ++i) - pTileTypes[i].uTileID = pTileTable->tileIdForTileset(pTileTypes[i].tileset, 1); +template +static bool contains(const Image &image, Pointi point) { + return point.x >= 0 && point.x < image.width() && point.y >= 0 && point.y <= image.height(); } -void OutdoorTerrain::CreateDebugTerrain() { - ZeroLandscape(); - pTileTypes[0].tileset = TILE_SET_GRASS; - pTileTypes[1].tileset = TILE_SET_WATER; - pTileTypes[2].tileset = TILE_SET_BADLANDS; - pTileTypes[3].tileset = TILE_SET_ROAD_GRASS_COBBLE; - LoadBaseTileIds(); +OutdoorTerrain::OutdoorTerrain() { + // Map is 127x127 squares. + _heightMap = Image::solid(128, 128, 0); + _tileMap = Image::solid(127, 127, 0); + _originalTileMap = Image::solid(127, 127, 0); + _normalMap = Image>::solid(127, 127, {Vec3f(0, 0, 1), Vec3f(0, 0, 1)}); } -//----- (00488F2E) -------------------------------------------------------- -//----- (0047EE16) -------------------------------------------------------- -int OutdoorTerrain::heightByGrid(Vec2i gridPos) { - if (gridPos.x < 0 || gridPos.x > 127 || gridPos.y < 0 || gridPos.y > 127) +void OutdoorTerrain::createDebugTerrain() { + int tileId = pTileTable->tileId(TILESET_GRASS, TILE_VARIANT_BASE1); + + _heightMap.fill(0); + _tileMap.fill(tileId); + _normalMap.fill({Vec3f(0, 0, 1), Vec3f(0, 0, 1)}); + + _tilesets[0] = TILESET_GRASS; + _tilesets[1] = TILESET_WATER; + _tilesets[2] = TILESET_BADLANDS; + _tilesets[3] = TILESET_ROAD_GRASS_COBBLE; +} + +void OutdoorTerrain::changeSeason(int month) { + assert(month >= 0 && month <= 11); + std::ranges::transform(_originalTileMap.pixels(), _tileMap.pixels().begin(), [&] (int tileId) { + return tileIdForSeason(tileId, month); + }); +} + +int OutdoorTerrain::heightByGrid(Pointi gridPos) const { + if (!contains(_heightMap, gridPos)) return 0; - return 32 * pHeightmap[gridPos.y * 128 + gridPos.x]; + return 32 * _heightMap[gridPos]; } -int OutdoorTerrain::heightByPos(const Vec3f &pos) { +int OutdoorTerrain::heightByPos(const Vec3f &pos) const { // TODO(captainurist): This should return float. But we'll need to retrace. int originz; // ebx@11 int lz; // eax@11 @@ -52,227 +86,184 @@ int OutdoorTerrain::heightByPos(const Vec3f &pos) { // party would be jerked up upon coming ashore, and this just looks ugly. Find a way to // reimplement this properly. - Vec2i gridPos = WorldPosToGrid(pos); + Pointi gridPos = worldToGrid(pos); - OutdoorTileGeometry tile = pOutdoor->pTerrain.tileGeometryByGrid(gridPos); + TileGeometry tile = tileGeometryByGrid(gridPos); - if (tile.v00.z != tile.v10.z || tile.v10.z != tile.v11.z || tile.v11.z != tile.v01.z) { + if (tile.z00 != tile.z10 || tile.z10 != tile.z11 || tile.z11 != tile.z01) { // On a slope. - if (std::abs(tile.v00.y - pos.y) >= std::abs(pos.x - tile.v00.x)) { - originz = tile.v01.z; - lz = tile.v11.z; - rz = tile.v00.z; - lpos = pos.x - tile.v00.x; - rpos = pos.y - tile.v11.y; + if (std::abs(tile.v0.y - pos.y) >= std::abs(pos.x - tile.v0.x)) { + originz = tile.z01; + lz = tile.z11; + rz = tile.z00; + lpos = pos.x - tile.v0.x; + rpos = pos.y - tile.v1.y; } else { - originz = tile.v10.z; - lz = tile.v00.z; - rz = tile.v11.z; - lpos = tile.v11.x - pos.x; - rpos = tile.v00.y - pos.y; + originz = tile.z10; + lz = tile.z00; + rz = tile.z11; + lpos = tile.v1.x - pos.x; + rpos = tile.v0.y - pos.y; } - assert(lpos >= 0 && lpos < 512); - assert(rpos >= 0 && rpos < 512); + //assert(lpos >= 0 && lpos < 512); // TODO(captainurist): fails in rare cases b/c not all of our code is in floats + //assert(rpos >= 0 && rpos < 512); // (x >> 9) is basically (x / 512) but with consistent rounding towards -inf. return originz + ((rpos * (rz - originz)) >> 9) + ((lpos * (lz - originz)) >> 9); } else { // On flat terrain. - return tile.v00.z; + return tile.z00; } } -int OutdoorTerrain::tileIdByGrid(Vec2i gridPos) const { - if (gridPos.x < 0 || gridPos.x > 127 || gridPos.y < 0 || gridPos.y > 127) +int OutdoorTerrain::tileIdByGrid(Pointi gridPos) const { + if (!contains(_tileMap, gridPos)) return 0; - return mapToGlobalTileId(pTilemap[gridPos.y * 128 + gridPos.x]); + return _tileMap[gridPos]; } -TileSet OutdoorTerrain::tileSetByGrid(Vec2i gridPos) const { - if (gridPos.x < 0 || gridPos.x > 127 || gridPos.y < 0 || gridPos.y > 127) - return TILE_SET_INVALID; - - int localTileId = pTilemap[gridPos.y * 128 + gridPos.x]; - - if (localTileId >= 1 && localTileId <= 12) - return TILE_SET_DIRT; // See comment in mapToGlobalTileId. +const TileData &OutdoorTerrain::tileDataByGrid(Pointi gridPos) const { + return pTileTable->tiles[tileIdByGrid(gridPos)]; +} - if (localTileId >= 234 || localTileId < 90) - return TILE_SET_INVALID; +Tileset OutdoorTerrain::tilesetByGrid(Pointi gridPos) const { + if (!contains(_tileMap, gridPos)) + return TILESET_INVALID; - int tileSetIndex = (localTileId - 90) / 36; - return pTileTypes[tileSetIndex].tileset; + return pTileTable->tiles[_tileMap[gridPos]].tileset; } -SoundId OutdoorTerrain::soundIdByGrid(Vec2i gridPos, bool isRunning) const { - // TODO(captainurist): this doesn't take seasons into account. - switch (tileSetByGrid(gridPos)) { - case TILE_SET_GRASS: - return isRunning ? SOUND_RunGrass : SOUND_WalkGrass; - case TILE_SET_SNOW: - return isRunning ? SOUND_RunSnow : SOUND_WalkSnow; - case TILE_SET_DESERT: - return isRunning ? SOUND_RunDesert : SOUND_WalkDesert; - case TILE_SET_COOLED_LAVA: - return isRunning ? SOUND_RunCooledLava : SOUND_WalkCooledLava; - case TILE_SET_INVALID: // Use dirt sounds for invalid tiles. - case TILE_SET_DIRT: - // Water sounds were used - return isRunning ? SOUND_RunDirt : SOUND_WalkDirt; - case TILE_SET_WATER: - // Dirt sounds were used - return isRunning ? SOUND_RunWater : SOUND_WalkWater; - case TILE_SET_BADLANDS: - return isRunning ? SOUND_RunBadlands : SOUND_WalkBadlands; - case TILE_SET_SWAMP: - return isRunning ? SOUND_RunSwamp : SOUND_WalkSwamp; - case TILE_SET_TROPICAL: - // TODO(Nik-RE-dev): is that correct? - return isRunning ? SOUND_RunGrass : SOUND_WalkGrass; - case TILE_SET_ROAD_GRASS_COBBLE: - case TILE_SET_ROAD_GRASS_DIRT: - case TILE_SET_ROAD_SNOW_COBBLE: - case TILE_SET_ROAD_SNOW_DIRT: - case TILE_SET_ROAD_SAND_COBBLE: - case TILE_SET_ROAD_SAND_DIRT: - case TILE_SET_ROAD_VOLCANO_COBBLE: - case TILE_SET_ROAD_VOLCANO_DIRT: - case TILE_SET_ROAD_CRACKED_COBBLE: - case TILE_SET_ROAD_CRACKED_DIRT: - case TILE_SET_ROAD_SWAMP_COBBLE: - case TILE_SET_ROAD_SWAMP_DIRT: - case TILE_SET_ROAD_TROPICAL_COBBLE: - case TILE_SET_ROAD_TROPICAL_DIRT: - return isRunning ? SOUND_RunRoad : SOUND_WalkRoad; - case TILE_SET_CITY: - case TILE_SET_ROAD_CITY_STONE: - // TODO(Nik-RE-dev): is that correct? - default: - return isRunning ? SOUND_RunGround : SOUND_WalkGround; - } +Tileset OutdoorTerrain::tilesetByPos(const Vec3f &pos) const { + return tilesetByGrid(worldToGrid(pos)); } -bool OutdoorTerrain::isWaterByGrid(Vec2i gridPos) const { +bool OutdoorTerrain::isWaterByGrid(Pointi gridPos) const { return pTileTable->tiles[tileIdByGrid(gridPos)].uAttributes & TILE_WATER; } -bool OutdoorTerrain::isWaterOrShoreByGrid(Vec2i gridPos) const { - return pTileTable->tiles[tileIdByGrid(gridPos)].uAttributes & (TILE_WATER | TILE_SHORE); +bool OutdoorTerrain::isWaterByPos(const Vec3f &pos) const { + return isWaterByGrid(worldToGrid(pos)); } -bool OutdoorTerrain::isWaterByPos(const Vec3f &pos) const { - return isWaterByGrid(WorldPosToGrid(pos)); +bool OutdoorTerrain::isWaterOrShoreByGrid(Pointi gridPos) const { + return pTileTable->tiles[tileIdByGrid(gridPos)].uAttributes & (TILE_WATER | TILE_SHORE); } Vec3f OutdoorTerrain::normalByPos(const Vec3f &pos) const { - Vec2i gridPos = WorldPosToGrid(pos); + Pointi gridPos = worldToGrid(pos); + if (!contains(_normalMap, gridPos)) + return Vec3f(0, 0, 1); - OutdoorTileGeometry tile = pOutdoor->pTerrain.tileGeometryByGrid(gridPos); + Vec2i o = gridToWorld(gridPos); + int dx = pos.x - o.x; + int dy = o.y - pos.y; - Vec3f side1, side2; + assert(dx >= 0); + assert(dy >= 0); - int dx = std::abs(pos.x - tile.v00.x); - int dy = std::abs(tile.v00.y - pos.y); if (dy >= dx) { - side2 = tile.v11 - tile.v01; - side1 = tile.v00 - tile.v01; - /* |\ - side1 | \ - |____\ - side 2 */ - } else { - side2 = tile.v00 - tile.v10; - side1 = tile.v11 - tile.v10; - /* side 2 - _____ - \ | - \ | side 1 - \| */ - } - - Vec3f n = cross(side2, side1); - float mag = n.length(); - if (fabsf(mag) < 1e-6f) { - return Vec3f(0, 0, 1); + return _normalMap[gridPos][1]; } else { - return n / mag; + return _normalMap[gridPos][0]; } } bool OutdoorTerrain::isSlopeTooHighByPos(const Vec3f &pos) const { - Vec2i gridPos = WorldPosToGrid(pos); + Pointi gridPos = worldToGrid(pos); + + TileGeometry tile = tileGeometryByGrid(gridPos); - OutdoorTileGeometry tile = pOutdoor->pTerrain.tileGeometryByGrid(gridPos); + int dx = pos.x - tile.v0.x; + int dy = tile.v0.y - pos.y; - int dx = std::abs(pos.x - tile.v00.x), dz = std::abs(tile.v00.y - pos.y); + assert(dx >= 0); + assert(dy >= 0); - int y1, y2, y3; - if (dz >= dx) { - y1 = tile.v01.z; - y2 = tile.v11.z; - y3 = tile.v00.z; + int z1, z2, z3; + if (dy >= dx) { // lower-left triangle - // y3 | \ + // z3 | \ // | \ // | \ // |______ \ - // y1 y2 + // z1 z2 + z1 = tile.z01; + z2 = tile.z11; + z3 = tile.z00; } else { - y1 = tile.v10.z; - y2 = tile.v00.z; - y3 = tile.v11.z; - - // upper-right - // y2_______ y1 + // upper-right triangle + // z2_______ z1 // \ | // \ | // \ | - // y3 + // z3 + z1 = tile.z10; + z2 = tile.z00; + z3 = tile.z11; } - int y_min = std::min(y1, std::min(y2, y3)); // не верно при подъёме на склон - int y_max = std::max(y1, std::max(y2, y3)); - return (y_max - y_min) > 512; + int yMin = std::min({z1, z2, z3}); + int yMax = std::max({z1, z2, z3}); + return yMax - yMin > 512; } -OutdoorTileGeometry OutdoorTerrain::tileGeometryByGrid(Vec2i gridPos) const { - int x0 = GridCellToWorldPosX(gridPos.x); - int y0 = GridCellToWorldPosY(gridPos.y); - int x1 = GridCellToWorldPosX(gridPos.x + 1); - int y1 = GridCellToWorldPosY(gridPos.y + 1); - - int z00 = pOutdoor->pTerrain.heightByGrid(gridPos); - int z01 = pOutdoor->pTerrain.heightByGrid(gridPos + Vec2i(0, 1)); - int z10 = pOutdoor->pTerrain.heightByGrid(gridPos + Vec2i(1, 0)); - int z11 = pOutdoor->pTerrain.heightByGrid(gridPos + Vec2i(1, 1)); - - OutdoorTileGeometry result; - result.v00 = Vec3f(x0, y0, z00); - result.v01 = Vec3f(x0, y1, z01); - result.v10 = Vec3f(x1, y0, z10); - result.v11 = Vec3f(x1, y1, z11); - return result; +void reconstruct(const OutdoorLocation_MM7 &src, OutdoorTerrain *dst) { + std::array baseTileIds; + for (int i = 0; i < 4; i++) { + dst->_tilesets[i] = static_cast(src.tileTypes[i].tileset); + baseTileIds[i] = pTileTable->tileId(dst->_tilesets[i], TILE_VARIANT_BASE1); + } + + for (int y = 0; y < 128; y++) + for (int x = 0; x < 128; x++) + dst->_heightMap[y][x] = src.heightMap[y * 128 + x]; + + for (int y = 0; y < 127; y++) + for (int x = 0; x < 127; x++) + dst->_originalTileMap[y][x] = mapToGlobalTileId(baseTileIds, src.tileMap[y * 128 + x]); + dst->_tileMap = Image::copy(dst->_originalTileMap); + + dst->recalculateNormals(); } -int OutdoorTerrain::mapToGlobalTileId(int localTileId) const { - // Tiles in tilemap: - // [0..90) are mapped as-is, but seem to be mostly invalid. Only global tile ids [1..12] are valid (all are dirt), - // the rest are "pending", effectively invalid. - // [90..126) map to tileset #1. - // [126..162) map to tileset #2. - // [162..198) map to tileset #3. - // [198..234) map to tileset #4 (road). - // [234..255) are invalid. +void OutdoorTerrain::recalculateNormals() { + for (int y = 0; y < _normalMap.height(); y++) { + for (int x = 0; x < _normalMap.width(); x++) { + TileGeometry tile = tileGeometryByGrid({x, y}); - if (localTileId < 90) - return localTileId; + Vec3f a2 = Vec3f(tile.v1.x, tile.v1.y, tile.z11) - Vec3f(tile.v0.x, tile.v1.y, tile.z01); + Vec3f a1 = Vec3f(tile.v0.x, tile.v0.y, tile.z00) - Vec3f(tile.v0.x, tile.v1.y, tile.z01); + Vec3f b2 = Vec3f(tile.v0.x, tile.v0.y, tile.z00) - Vec3f(tile.v1.x, tile.v0.y, tile.z10); + Vec3f b1 = Vec3f(tile.v1.x, tile.v1.y, tile.z11) - Vec3f(tile.v1.x, tile.v0.y, tile.z10); - if (localTileId >= 234) - return 0; + // TODO(captainurist): use normalize() & retrace. + + Vec3f an = cross(a2, a1); + float amag = an.length(); + an /= amag; - int tileSetIndex = (localTileId - 90) / 36; - int tileSetOffset = (localTileId - 90) % 36; - return pTileTypes[tileSetIndex].uTileID + tileSetOffset; + Vec3f bn = cross(b2, b1); + float bmag = bn.length(); + bn /= bmag; + + assert(an.z > 0); + assert(bn.z > 0); + + _normalMap[y][x][0] = bn; + _normalMap[y][x][1] = an; + } + } +} + +OutdoorTerrain::TileGeometry OutdoorTerrain::tileGeometryByGrid(Pointi gridPos) const { + TileGeometry result; + result.v0 = gridToWorld(gridPos); + result.v1 = gridToWorld(gridPos + Pointi(1, 1)); + result.z00 = heightByGrid(gridPos); + result.z01 = heightByGrid(gridPos + Pointi(0, 1)); + result.z10 = heightByGrid(gridPos + Pointi(1, 0)); + result.z11 = heightByGrid(gridPos + Pointi(1, 1)); + return result; } diff --git a/src/Engine/Graphics/OutdoorTerrain.h b/src/Engine/Graphics/OutdoorTerrain.h index 791cbf51a673..abb76c7b019a 100644 --- a/src/Engine/Graphics/OutdoorTerrain.h +++ b/src/Engine/Graphics/OutdoorTerrain.h @@ -1,89 +1,149 @@ #pragma once #include -#include -#include "Library/Geometry/Vec.h" +#include "Library/Geometry/Point.h" +#include "Library/Image/Image.h" #include "Engine/Data/TileEnums.h" -#include "Media/Audio/SoundEnums.h" - -struct OutdoorTileType { - TileSet tileset = TILE_SET_INVALID; - uint16_t uTileID = 0; -}; - -struct OutdoorTileGeometry { - Vec3f v00, v01, v10, v11; // Four vertices of the tile, v00 is at (x0, y0), v01 at (x0, y1), etc. -}; - +struct TileData; +struct OutdoorLocation_MM7; + +/** + * A note on grid coordinates. In grid coordinates Y-axis points south (down on the minimap), X-axis points east (right + * on the minimap), `(0, 0)` is NW (top-left) corner of the map. + * + * @param gridPos Grid coordinates. + * @return World coordinates of the grid cell's NW (top-left on the minimap) corner. + * @offset 0x0047F469, 0x0047F476 + */ +inline Vec2i gridToWorld(Pointi gridPos) { + return {(gridPos.x - 64) << 9, (64 - gridPos.y) << 9}; +} + +/** + * A note on world coordinates. Y-axis points north (up on the minimap), X-axis points east (right on the minimap), + * `(0, 0)` is in the center of the map. + * + * @param worldPos Position in world coordinates. + * @return Grid cell coordinates that `worldPos` is in. + * @offset 0x0047F44B, 0x0047F458 + */ +inline Pointi worldToGrid(const Vec3f &worldPos) { + int worldX = worldPos.x; + int worldY = worldPos.y; + + // sar is in original exe, resulting -880 / 512 = -1 and -880 sar 9 = -2. + int gridX = (worldX >> 9) + 64; + int gridY = 63 - (worldY >> 9); + return Pointi(gridX, gridY); +} + +/** + * Terrain for outdoor location. + * + * Contains geometry & tile data, and provides some convenience methods for accessing it. + * + * All methods do bounds checking, so this class effectively presents a view into a location that's infinite in size. + * Methods ending with `-Unsafe` don't do bounds checking. + */ class OutdoorTerrain { public: - bool ZeroLandscape(); - void LoadBaseTileIds(); - void CreateDebugTerrain(); + OutdoorTerrain(); - int heightByGrid(Vec2i gridPos); + void createDebugTerrain(); + + void changeSeason(int month); + + /** + * @param gridPos Grid coordinates. + * @return Terrain height at `gridPos`. + * @offset 0x00488F2E, 0x0047EE16 + */ + [[nodiscard]] int heightByGrid(Pointi gridPos) const; /** + * @param pos World coordinates, only xy component is used by this function. + * @return Terrain height at given position. * @offset 0x0048257A */ - int heightByPos(const Vec3f &pos); + [[nodiscard]] int heightByPos(const Vec3f &pos) const; + + [[nodiscard]] Vec3i vertexByGridUnsafe(Pointi gridPos) const { + Vec2i tmp = gridToWorld(gridPos); + return Vec3i(tmp.x, tmp.y, 32 * _heightMap[gridPos]); + } /** * @param gridPos Grid coordinates. * @return Tile id at `gridPos` that can then be used to get tile data from `TileTable`. */ - int tileIdByGrid(Vec2i gridPos) const; + [[nodiscard]] int tileIdByGrid(Pointi gridPos) const; /** * @param gridPos Grid coordinates. - * @return Tile set for the tile at `gridPos`, or `Tileset_NULL` if the tile is invalid. + * @return Tile data from the global tile table for the tile at `gridPos`. + * @offset 0x47ED08 */ - TileSet tileSetByGrid(Vec2i gridPos) const; + [[nodiscard]] const TileData &tileDataByGrid(Pointi gridPos) const; /** - * @offset 0x47EE49 + * @param gridPos Grid coordinates. + * @return Tile set for the tile at `gridPos`, or `Tileset_NULL` if the tile is invalid. */ - SoundId soundIdByGrid(Vec2i gridPos, bool isRunning) const; + [[nodiscard]] Tileset tilesetByGrid(Pointi gridPos) const; + + [[nodiscard]] Tileset tilesetByPos(const Vec3f &pos) const; /** * @param gridPos Grid coordinates. * @return Whether the tile at `gridPos` is a water tile. Note that shore tiles are * different from water tiles. */ - bool isWaterByGrid(Vec2i gridPos) const; + [[nodiscard]] bool isWaterByGrid(Pointi gridPos) const; - bool isWaterOrShoreByGrid(Vec2i gridPos) const; + [[nodiscard]] bool isWaterByPos(const Vec3f &pos) const; - bool isWaterByPos(const Vec3f &pos) const; + [[nodiscard]] bool isWaterOrShoreByGrid(Pointi gridPos) const; /** - * * @param pos World coordinates, only xy component is used by this function. * @return Terrain normal at given position. Terrain normals always point up (`z > 0`). * @offset 0x0046DCC8 */ - Vec3f normalByPos(const Vec3f &pos) const; + [[nodiscard]] Vec3f normalByPos(const Vec3f &pos) const; + + [[nodiscard]] const std::array &normalsByGridUnsafe(Pointi gridPos) const { + return _normalMap[gridPos]; + } /** * @param pos World coordinates, only xy component is used by this function. * @return Whether terrain slope at given position is too high to be climbed or stood on. * @offset 0x004823F4 */ - bool isSlopeTooHighByPos(const Vec3f &pos) const; + [[nodiscard]] bool isSlopeTooHighByPos(const Vec3f &pos) const; - // TODO(captainurist): also move all the functions that use this method into this class. - OutdoorTileGeometry tileGeometryByGrid(Vec2i gridPos) const; + friend void reconstruct(const OutdoorLocation_MM7 &src, OutdoorTerrain *dst); - std::array pTileTypes; // [3] is road tileset. - std::array pHeightmap = {}; - std::array pTilemap = {}; - std::array pAttributemap = {}; - std::vector pTerrainNormals; - std::array pTerrainNormalIndices; + private: + struct TileGeometry { + Vec2i v0; + Vec2i v1; // We have a retarded coordinate system, so v1.y < v0.y, always. + int z00 = 0; + int z01 = 0; + int z10 = 0; + int z11 = 0; + }; + + void recalculateNormals(); + [[nodiscard]] TileGeometry tileGeometryByGrid(Pointi gridPos) const; private: - int mapToGlobalTileId(int localTileId) const; + std::array _tilesets; // Tileset ids used in this location, [3] is road tileset. + Image _heightMap; // Height map, to get actual height multiply by 32. + Image _tileMap; // Tile id map, indices into the global tile table. + Image _originalTileMap; // Same as above, but w/o seasonal changes. + Image> _normalMap; // Terrain normal map, two normals per tile for two triangles. }; diff --git a/src/Engine/Graphics/Renderer/OpenGLRenderer.cpp b/src/Engine/Graphics/Renderer/OpenGLRenderer.cpp index 2452897e89fd..bb67689b0861 100644 --- a/src/Engine/Graphics/Renderer/OpenGLRenderer.cpp +++ b/src/Engine/Graphics/Renderer/OpenGLRenderer.cpp @@ -1455,17 +1455,11 @@ void OpenGLRenderer::DrawOutdoorTerrain() { // generate array and populate data if (terrainVAO == 0) { static RenderVertexSoft pTerrainVertices[128 * 128]; - int blockScale = 512; - int heightScale = 32; // generate vertex locations - for (unsigned int y = 0; y < 128; ++y) { - for (unsigned int x = 0; x < 128; ++x) { - pTerrainVertices[y * 128 + x].vWorldPosition.x = (-64.0f + x) * blockScale; - pTerrainVertices[y * 128 + x].vWorldPosition.y = (64.0f - y) * blockScale; - pTerrainVertices[y * 128 + x].vWorldPosition.z = heightScale * pOutdoor->pTerrain.pHeightmap[y * 128 + x]; - } - } + for (int y = 0; y < 128; ++y) + for (int x = 0; x < 128; ++x) + pTerrainVertices[y * 128 + x].vWorldPosition = pOutdoor->pTerrain.vertexByGridUnsafe({x, y}).toFloat(); // reserve first 7 layers for water tiles in unit 0 auto wtrtexture = this->hd_water_tile_anim[0]; @@ -1482,24 +1476,24 @@ void OpenGLRenderer::DrawOutdoorTerrain() { // map is 127 x 127 squares - each square has two triangles - each tri has 3 verts // first find all required textures for terrain and add to map - auto tile = pOutdoor->getTileDescByGrid(x, y); + const auto &tile = pOutdoor->pTerrain.tileDataByGrid({x, y}); int tileunit = 0; int tilelayer = 0; // check if tile->name is already in list - auto mapiter = terraintexmap.find(tile->name); + auto mapiter = terraintexmap.find(tile.name); if (mapiter != terraintexmap.end()) { // if so, extract unit and layer int unitlayer = mapiter->second; tilelayer = unitlayer & 0xFF; tileunit = (unitlayer & 0xFF00) >> 8; - } else if (tile->name == "wtrtyl") { + } else if (tile.name == "wtrtyl") { // water tile tileunit = 0; tilelayer = 0; } else { // else need to add it - auto thistexture = assets->getBitmap(tile->name); + auto thistexture = assets->getBitmap(tile.name); int width = thistexture->width(); // check size to see what unit it needs int i; @@ -1521,7 +1515,7 @@ void OpenGLRenderer::DrawOutdoorTerrain() { if (numterraintexloaded[i] < 256) { // intsert into tex map - terraintexmap.insert(std::make_pair(tile->name, encode)); + terraintexmap.insert(std::make_pair(tile.name, encode)); numterraintexloaded[i]++; } else { logger->warning("Texture layer full - draw terrain!"); @@ -1532,12 +1526,7 @@ void OpenGLRenderer::DrawOutdoorTerrain() { } // next calculate all vertices vertices - unsigned norm_idx = pOutdoor->pTerrain.pTerrainNormalIndices[(2 * x * 128) + (2 * y) + 2 /*+ 1*/]; // 2 is top tri // 3 is bottom - unsigned bottnormidx = pOutdoor->pTerrain.pTerrainNormalIndices[(2 * x * 128) + (2 * y) + 3]; - assert(norm_idx < pOutdoor->pTerrain.pTerrainNormals.size()); - assert(bottnormidx < pOutdoor->pTerrain.pTerrainNormals.size()); - Vec3f *norm = &pOutdoor->pTerrain.pTerrainNormals[norm_idx]; - Vec3f *norm2 = &pOutdoor->pTerrain.pTerrainNormals[bottnormidx]; + const auto &[norm, norm2] = pOutdoor->pTerrain.normalsByGridUnsafe({x, y}); // calc each vertex // [0] - x,y n1 @@ -1548,9 +1537,9 @@ void OpenGLRenderer::DrawOutdoorTerrain() { terrshaderstore[6 * (x + (127 * y))].v = 0; terrshaderstore[6 * (x + (127 * y))].texunit = tileunit; terrshaderstore[6 * (x + (127 * y))].texturelayer = tilelayer; - terrshaderstore[6 * (x + (127 * y))].normx = norm->x; - terrshaderstore[6 * (x + (127 * y))].normy = norm->y; - terrshaderstore[6 * (x + (127 * y))].normz = norm->z; + terrshaderstore[6 * (x + (127 * y))].normx = norm.x; + terrshaderstore[6 * (x + (127 * y))].normy = norm.y; + terrshaderstore[6 * (x + (127 * y))].normz = norm.z; terrshaderstore[6 * (x + (127 * y))].attribs = 0; // [1] - x+1,y+1 n1 @@ -1561,9 +1550,9 @@ void OpenGLRenderer::DrawOutdoorTerrain() { terrshaderstore[6 * (x + (127 * y)) + 1].v = 1; terrshaderstore[6 * (x + (127 * y)) + 1].texunit = tileunit; terrshaderstore[6 * (x + (127 * y)) + 1].texturelayer = tilelayer; - terrshaderstore[6 * (x + (127 * y)) + 1].normx = norm->x; - terrshaderstore[6 * (x + (127 * y)) + 1].normy = norm->y; - terrshaderstore[6 * (x + (127 * y)) + 1].normz = norm->z; + terrshaderstore[6 * (x + (127 * y)) + 1].normx = norm.x; + terrshaderstore[6 * (x + (127 * y)) + 1].normy = norm.y; + terrshaderstore[6 * (x + (127 * y)) + 1].normz = norm.z; terrshaderstore[6 * (x + (127 * y)) + 1].attribs = 0; // [2] - x+1,y n1 @@ -1574,9 +1563,9 @@ void OpenGLRenderer::DrawOutdoorTerrain() { terrshaderstore[6 * (x + (127 * y)) + 2].v = 0; terrshaderstore[6 * (x + (127 * y)) + 2].texunit = tileunit; terrshaderstore[6 * (x + (127 * y)) + 2].texturelayer = tilelayer; - terrshaderstore[6 * (x + (127 * y)) + 2].normx = norm->x; - terrshaderstore[6 * (x + (127 * y)) + 2].normy = norm->y; - terrshaderstore[6 * (x + (127 * y)) + 2].normz = norm->z; + terrshaderstore[6 * (x + (127 * y)) + 2].normx = norm.x; + terrshaderstore[6 * (x + (127 * y)) + 2].normy = norm.y; + terrshaderstore[6 * (x + (127 * y)) + 2].normz = norm.z; terrshaderstore[6 * (x + (127 * y)) + 2].attribs = 0; // [3] - x,y n2 @@ -1587,9 +1576,9 @@ void OpenGLRenderer::DrawOutdoorTerrain() { terrshaderstore[6 * (x + (127 * y)) + 3].v = 0; terrshaderstore[6 * (x + (127 * y)) + 3].texunit = tileunit; terrshaderstore[6 * (x + (127 * y)) + 3].texturelayer = tilelayer; - terrshaderstore[6 * (x + (127 * y)) + 3].normx = norm2->x; - terrshaderstore[6 * (x + (127 * y)) + 3].normy = norm2->y; - terrshaderstore[6 * (x + (127 * y)) + 3].normz = norm2->z; + terrshaderstore[6 * (x + (127 * y)) + 3].normx = norm2.x; + terrshaderstore[6 * (x + (127 * y)) + 3].normy = norm2.y; + terrshaderstore[6 * (x + (127 * y)) + 3].normz = norm2.z; terrshaderstore[6 * (x + (127 * y)) + 3].attribs = 0; // [4] - x,y+1 n2 @@ -1600,9 +1589,9 @@ void OpenGLRenderer::DrawOutdoorTerrain() { terrshaderstore[6 * (x + (127 * y)) + 4].v = 1; terrshaderstore[6 * (x + (127 * y)) + 4].texunit = tileunit; terrshaderstore[6 * (x + (127 * y)) + 4].texturelayer = tilelayer; - terrshaderstore[6 * (x + (127 * y)) + 4].normx = norm2->x; - terrshaderstore[6 * (x + (127 * y)) + 4].normy = norm2->y; - terrshaderstore[6 * (x + (127 * y)) + 4].normz = norm2->z; + terrshaderstore[6 * (x + (127 * y)) + 4].normx = norm2.x; + terrshaderstore[6 * (x + (127 * y)) + 4].normy = norm2.y; + terrshaderstore[6 * (x + (127 * y)) + 4].normz = norm2.z; terrshaderstore[6 * (x + (127 * y)) + 4].attribs = 0; // [5] - x+1,y+1 n2 @@ -1613,9 +1602,9 @@ void OpenGLRenderer::DrawOutdoorTerrain() { terrshaderstore[6 * (x + (127 * y)) + 5].v = 1; terrshaderstore[6 * (x + (127 * y)) + 5].texunit = tileunit; terrshaderstore[6 * (x + (127 * y)) + 5].texturelayer = tilelayer; - terrshaderstore[6 * (x + (127 * y)) + 5].normx = norm2->x; - terrshaderstore[6 * (x + (127 * y)) + 5].normy = norm2->y; - terrshaderstore[6 * (x + (127 * y)) + 5].normz = norm2->z; + terrshaderstore[6 * (x + (127 * y)) + 5].normx = norm2.x; + terrshaderstore[6 * (x + (127 * y)) + 5].normy = norm2.y; + terrshaderstore[6 * (x + (127 * y)) + 5].normz = norm2.z; terrshaderstore[6 * (x + (127 * y)) + 5].attribs = 0; } } @@ -1888,7 +1877,7 @@ void OpenGLRenderer::DrawOutdoorTerrain() { // loop over blood to lay for (unsigned i = 0; i < NumBloodsplats; ++i) { // approx location of bloodsplat - Vec2i gridPos = WorldPosToGrid(decal_builder->bloodsplat_container->pBloodsplats_to_apply[i].pos); + Vec2i gridPos = worldToGrid(decal_builder->bloodsplat_container->pBloodsplats_to_apply[i].pos); // use terrain squares in block surrounding to try and stack faces int scope = std::ceil(decal_builder->bloodsplat_container->pBloodsplats_to_apply[i].radius / 512); @@ -1948,32 +1937,27 @@ void OpenGLRenderer::DrawOutdoorTerrain() { // splat hits this square of terrain bool fading = pOutdoor->pTerrain.isWaterOrShoreByGrid({loopx, loopy}); - unsigned norm_idx = pOutdoor->pTerrain.pTerrainNormalIndices[(2 * loopx * 128) + (2 * loopy) + 2]; // 2 is top tri // 3 is bottom - unsigned bottnormidx = pOutdoor->pTerrain.pTerrainNormalIndices[(2 * loopx * 128) + (2 * loopy) + 3]; - assert(norm_idx < pOutdoor->pTerrain.pTerrainNormals.size()); - assert(bottnormidx < pOutdoor->pTerrain.pTerrainNormals.size()); - Vec3f *norm = &pOutdoor->pTerrain.pTerrainNormals[norm_idx]; - Vec3f *norm2 = &pOutdoor->pTerrain.pTerrainNormals[bottnormidx]; + const auto &[norm, norm2] = pOutdoor->pTerrain.normalsByGridUnsafe({loopx, loopy}); float Light_tile_dist = 0.0; // top tri - float _f1 = norm->x * pOutdoor->vSunlight.x + norm->y * pOutdoor->vSunlight.y + norm->z * pOutdoor->vSunlight.z; + float _f1 = norm.x * pOutdoor->vSunlight.x + norm.y * pOutdoor->vSunlight.y + norm.z * pOutdoor->vSunlight.z; int dimming_level = std::clamp(static_cast(20.0f - floorf(20.0f * _f1 + 0.5f)), 0, 31); decal_builder->ApplyBloodSplatToTerrain(fading, norm, &Light_tile_dist, VertexRenderList, i); Planef plane; - plane.normal = *norm; + plane.normal = norm; plane.dist = Light_tile_dist; if (decal_builder->uNumSplatsThisFace > 0) decal_builder->BuildAndApplyDecals(31 - dimming_level, LocationTerrain, plane, 3, VertexRenderList, 0, -1); //bottom tri - float _f = norm2->x * pOutdoor->vSunlight.x + norm2->y * pOutdoor->vSunlight.y + norm2->z * pOutdoor->vSunlight.z; + float _f = norm2.x * pOutdoor->vSunlight.x + norm2.y * pOutdoor->vSunlight.y + norm2.z * pOutdoor->vSunlight.z; dimming_level = std::clamp(static_cast(20.0 - floorf(20.0 * _f + 0.5f)), 0, 31); decal_builder->ApplyBloodSplatToTerrain(fading, norm2, &Light_tile_dist, (VertexRenderList + 3), i); - plane.normal = *norm2; + plane.normal = norm2; plane.dist = Light_tile_dist; if (decal_builder->uNumSplatsThisFace > 0) decal_builder->BuildAndApplyDecals(31 - dimming_level, LocationTerrain, plane, 3, (VertexRenderList + 3), 0, -1); diff --git a/src/Engine/Graphics/Sprites.cpp b/src/Engine/Graphics/Sprites.cpp index b4cbd9f4df8d..3aab20c234f2 100644 --- a/src/Engine/Graphics/Sprites.cpp +++ b/src/Engine/Graphics/Sprites.cpp @@ -10,6 +10,7 @@ #include "Engine/Graphics/PaletteManager.h" #include "Engine/Graphics/Image.h" #include "Engine/LodSpriteCache.h" +#include "Engine/Seasons.h" #include "Library/Logger/Logger.h" @@ -213,82 +214,10 @@ void SpriteFrameTable::ResetPaletteIndexes() { } SpriteFrame *LevelDecorationChangeSeason(const DecorationDesc *desc, Duration t, int month) { - switch (month/*pParty->uCurrentMonth*/) { - // case 531 (tree60), 536 (tree65), 537 (tree66) have no autumn/winter - // sprites - case 11: - case 0: - case 1: // winter - { - switch (desc->uSpriteID) { - // case 468: //bush02 grows on swamps, which are - // evergreeen actually - case 548: // flower10 - case 547: // flower09 - case 541: // flower03 - case 539: // flower01 - return nullptr; - - case 483: // tree01 - case 486: // tree04 - case 492: // tree10 - { - pSpriteFrameTable->InitializeSprite(desc->uSpriteID + 2); - return pSpriteFrameTable->GetFrame(desc->uSpriteID + 2, t); - } - - default: - return pSpriteFrameTable->GetFrame(desc->uSpriteID, t); - } - } - - case 2: - case 3: - case 4: // spring - { - // switch (desc->uSpriteID) {} - return pSpriteFrameTable->GetFrame(desc->uSpriteID, t); - } - - case 8: - case 9: - case 10: // autumn - { - switch (desc->uSpriteID) { - // case 468: //bush02 grows on swamps, which are evergreeen - // actually - case 548: // flower10 - case 547: // flower09 - case 541: // flower03 - case 539: // flower01 - return nullptr; - - case 483: // tree01 - case 486: // tree04 - case 492: // tree10 - { - pSpriteFrameTable->InitializeSprite(desc->uSpriteID + 1); - return pSpriteFrameTable->GetFrame(desc->uSpriteID + 1, t); - } - - default: - return pSpriteFrameTable->GetFrame(desc->uSpriteID, t); - } - } break; - - case 5: - case 6: - case 7: // summer - // all green by default - { - return pSpriteFrameTable->GetFrame(desc->uSpriteID, t); - } - - default: - assert(/*pParty->uCurrentMonth*/month >= 0 && /*pParty->uCurrentMonth*/month < 12); - } - logger->warning("No sprite returned - LevelDecorationChangeSeason!"); - return nullptr; + int spriteId = spriteIdForSeason(desc->uSpriteID, month); + if (spriteId != desc->uSpriteID) + pSpriteFrameTable->InitializeSprite(spriteId); + return pSpriteFrameTable->GetFrame(spriteId, t); } int SpriteFrame::GetPaletteIndex() { diff --git a/src/Engine/Objects/SpriteObject.cpp b/src/Engine/Objects/SpriteObject.cpp index 0fa33c6a7236..2a5e3ab3bed2 100644 --- a/src/Engine/Objects/SpriteObject.cpp +++ b/src/Engine/Objects/SpriteObject.cpp @@ -226,7 +226,7 @@ void SpriteObject::updateObjectODM(unsigned int uLayingItemID) { } CollideOutdoorWithModels(false); - CollideOutdoorWithDecorations(WorldPosToGrid(pSpriteObjects[uLayingItemID].vPosition)); + CollideOutdoorWithDecorations(worldToGrid(pSpriteObjects[uLayingItemID].vPosition)); ObjectType casterType = pSpriteObjects[uLayingItemID].spell_caster_pid.type(); if (casterType != OBJECT_Character) { CollideWithParty(false); diff --git a/src/Engine/Seasons.cpp b/src/Engine/Seasons.cpp new file mode 100644 index 000000000000..3fc7e034e5bd --- /dev/null +++ b/src/Engine/Seasons.cpp @@ -0,0 +1,95 @@ +#include "Seasons.h" + +#include + +int tileIdForSeason(int tileId, int month) { + switch (month) { + case 11: + case 0: + case 1: // winter + if (tileId >= 90 && tileId <= 113) { // TILESET_GRASS + if (tileId <= 95) // some grastyl entries + return 348; + return 348 + (tileId - 96); + } + return tileId; + + case 2: + case 3: + case 4: // spring + case 8: + case 9: + case 10: // autumn + if (tileId >= 90 && tileId <= 113) // just convert all TILESET_GRASS to dirt + return 1; + return tileId; + + default: + assert(false); + [[fallthrough]]; + case 5: + case 6: + case 7: // summer + // All tiles are green grass by default. + return tileId; + } +} + +int spriteIdForSeason(int spriteId, int month) { + switch (month) { + // case 531 (tree60), 536 (tree65), 537 (tree66) have no autumn/winter + // sprites + case 11: + case 0: + case 1: // winter + switch (spriteId) { + // case 468: // bush02 grows on swamps, which are evergreeen actually + case 548: // flower10 + case 547: // flower09 + case 541: // flower03 + case 539: // flower01 + return 0; // null sprite + + case 483: // tree01 + case 486: // tree04 + case 492: // tree10 + return spriteId + 2; + + default: + return spriteId; + } + + case 2: + case 3: + case 4: // spring + return spriteId; + + case 8: + case 9: + case 10: // autumn + switch (spriteId) { + // case 468: // bush02 grows on swamps, which are evergreeen actually + case 548: // flower10 + case 547: // flower09 + case 541: // flower03 + case 539: // flower01 + return 0; // null sprite + + case 483: // tree01 + case 486: // tree04 + case 492: // tree10 + return spriteId + 1; + + default: + return spriteId; + } + + default: + assert(false); + [[fallthrough]]; + case 5: + case 6: + case 7: + return spriteId; + } +} diff --git a/src/Engine/Seasons.h b/src/Engine/Seasons.h new file mode 100644 index 000000000000..4abb50708e18 --- /dev/null +++ b/src/Engine/Seasons.h @@ -0,0 +1,4 @@ +#pragma once + +int tileIdForSeason(int tileId, int month); +int spriteIdForSeason(int spriteId, int month); diff --git a/src/Engine/Snapshots/CompositeSnapshots.cpp b/src/Engine/Snapshots/CompositeSnapshots.cpp index 87171ca1fb0c..d429df9c4fc4 100644 --- a/src/Engine/Snapshots/CompositeSnapshots.cpp +++ b/src/Engine/Snapshots/CompositeSnapshots.cpp @@ -357,16 +357,7 @@ void reconstruct(const OutdoorLocation_MM7 &src, OutdoorLocation *dst) { reconstruct(src.fileName, &dst->location_filename); reconstruct(src.desciption, &dst->location_file_description); reconstruct(src.skyTexture, &dst->sky_texture_filename); - // src.groundTilesetUnused is just dropped - reconstruct(src.tileTypes, &dst->pTerrain.pTileTypes); - dst->pTerrain.LoadBaseTileIds(); - - reconstruct(src.heightMap, &dst->pTerrain.pHeightmap); - reconstruct(src.tileMap, &dst->pTerrain.pTilemap); - reconstruct(src.attributeMap, &dst->pTerrain.pAttributemap); - - reconstruct(src.normalMap, &dst->pTerrain.pTerrainNormalIndices); - reconstruct(src.normals, &dst->pTerrain.pTerrainNormals); + reconstruct(src, &dst->pTerrain); dst->pBModels.clear(); for (size_t i = 0; i < src.models.size(); i++) { diff --git a/src/Engine/Snapshots/CompositeSnapshots.h b/src/Engine/Snapshots/CompositeSnapshots.h index 3103939a4588..fc8af02b89a1 100644 --- a/src/Engine/Snapshots/CompositeSnapshots.h +++ b/src/Engine/Snapshots/CompositeSnapshots.h @@ -13,6 +13,7 @@ * Struct fields are laid out in the order in which they are laid out in binary files. */ +// TODO(captainurist): snapshot/reconstruct functions belong to the classes themselves. Also drop raw* functions. class Blob; class BSPModel; @@ -88,9 +89,9 @@ struct OutdoorLocation_MM7 { std::array heightMap; std::array tileMap; std::array attributeMap; - uint32_t normalCount; + uint32_t normalCount; // Number of elements in `normals`. std::array someOtherMap; // Not used in OE, not even sure what this is. - std::array normalMap; + std::array normalMap; // Indices into `normals`, unused as we recalculate normals on load. std::vector normals; std::vector models; std::vector modelExtras; diff --git a/src/Engine/Snapshots/EntitySnapshots.cpp b/src/Engine/Snapshots/EntitySnapshots.cpp index 6c33f2f39756..ddeb05c08b6f 100644 --- a/src/Engine/Snapshots/EntitySnapshots.cpp +++ b/src/Engine/Snapshots/EntitySnapshots.cpp @@ -297,7 +297,7 @@ void reconstruct(const TileData_MM7 &src, TileData *dst) { dst->name.insert(0, "h"); // mm7 uses hd water tiles with legacy names dst->uTileID = src.tileId; - dst->tileset = static_cast(src.tileSet); + dst->tileset = static_cast(src.tileset); dst->uSection = static_cast(src.section); dst->uAttributes = static_cast(src.attributes); } @@ -1797,11 +1797,6 @@ void reconstruct(const PersistentVariables_MM7 &src, PersistentVariables *dst) { dst->decorVars = src.decorVars; } -void reconstruct(const OutdoorTileType_MM7 &src, OutdoorTileType *dst) { - dst->tileset = static_cast(src.tileset); - dst->uTileID = src.tileId; -} - void snapshot(const SaveGameHeader &src, SaveGameHeader_MM7 *dst) { memzero(dst); diff --git a/src/Engine/Snapshots/EntitySnapshots.h b/src/Engine/Snapshots/EntitySnapshots.h index 62e0d23d8269..4a7cf21ae87b 100644 --- a/src/Engine/Snapshots/EntitySnapshots.h +++ b/src/Engine/Snapshots/EntitySnapshots.h @@ -176,7 +176,7 @@ struct TileData_MM7 { std::array tileName; uint16_t tileId; uint16_t bitmapId; - uint16_t tileSet; + uint16_t tileset; uint16_t section; uint16_t attributes; }; @@ -1360,8 +1360,6 @@ struct OutdoorTileType_MM7 { static_assert(sizeof(OutdoorTileType_MM7) == 4); MM_DECLARE_MEMCOPY_SERIALIZABLE(OutdoorTileType_MM7) -void reconstruct(const OutdoorTileType_MM7 &src, OutdoorTileType *dst); - struct SaveGameHeader_MM7 { std::array name; diff --git a/src/Engine/Tables/TileTable.cpp b/src/Engine/Tables/TileTable.cpp index e2a038ff3211..50f636156bff 100644 --- a/src/Engine/Tables/TileTable.cpp +++ b/src/Engine/Tables/TileTable.cpp @@ -2,15 +2,16 @@ #include "Engine/AssetsManager.h" #include "Engine/Random/Random.h" +#include "Engine/Data/TileEnumFunctions.h" TileTable *pTileTable; //----- (00487ED6) -------------------------------------------------------- -int TileTable::tileIdForTileset(TileSet terrain_type, bool nonRandom) { +int TileTable::tileIdForTileset(Tileset terrain_type, bool nonRandom) { int v5; // edx@3 int v6; // edx@11 - if (nonRandom || terrain_type > TILE_SET_TROPICAL) { + if (nonRandom || terrain_type > TILESET_TROPICAL) { return tileId(terrain_type, TILE_VARIANT_BASE1); } v5 = vrng->random(50); @@ -23,11 +24,11 @@ int TileTable::tileIdForTileset(TileSet terrain_type, bool nonRandom) { } else if (v5 < 48) { return tileId(terrain_type, TILE_VARIANT_BASE4_NE); } - return tileId(terrain_type, vrng->randomSample(allSpecialTileSects())); + return tileId(terrain_type, vrng->randomSample(allSpecialTileVariants())); } //----- (00487F84) -------------------------------------------------------- -int TileTable::tileId(TileSet tileset, TileVariant section) { +int TileTable::tileId(Tileset tileset, TileVariant section) { for (size_t i = 0; i < tiles.size(); ++i) { if ((tiles[i].tileset == tileset) && (tiles[i].uSection == section)) diff --git a/src/Engine/Tables/TileTable.h b/src/Engine/Tables/TileTable.h index 77c83ac22dc0..fe1bae73d70f 100644 --- a/src/Engine/Tables/TileTable.h +++ b/src/Engine/Tables/TileTable.h @@ -5,8 +5,8 @@ #include "Engine/Data/TileData.h" struct TileTable { - int tileIdForTileset(TileSet tileset, bool nonRandom); - int tileId(TileSet tileset, TileVariant section); + int tileIdForTileset(Tileset tileset, bool nonRandom); + int tileId(Tileset tileset, TileVariant section); std::vector tiles; // Tile by id. }; diff --git a/src/Library/Config/AnyConfigEntry.cpp b/src/Library/Config/AnyConfigEntry.cpp index 67bbdbeee63f..558929b10b60 100644 --- a/src/Library/Config/AnyConfigEntry.cpp +++ b/src/Library/Config/AnyConfigEntry.cpp @@ -34,6 +34,9 @@ void AnyConfigEntry::setValue(std::any value) { return; _value = std::move(value); + + for (const auto &[_, listener] : _listeners) + listener(); } std::string AnyConfigEntry::defaultString() const { diff --git a/src/Library/Config/AnyConfigEntry.h b/src/Library/Config/AnyConfigEntry.h index 687d264c52a9..ac693e5d4a7e 100644 --- a/src/Library/Config/AnyConfigEntry.h +++ b/src/Library/Config/AnyConfigEntry.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "ConfigFwd.h" @@ -51,6 +52,14 @@ class AnyConfigEntry { return _description; } + void addListener(void *ctx, std::function listener) { + _listeners.emplace_back(ctx, std::move(listener)); + } + + void removeListeners(void *ctx) { + std::erase_if(_listeners, [ctx] (const auto &pair) { return pair.first == ctx;}); + } + protected: Validator validator() const { return _validator; @@ -64,4 +73,5 @@ class AnyConfigEntry { std::any _defaultValue; std::any _value; Validator _validator = nullptr; + std::vector>> _listeners; }; diff --git a/src/Library/Config/ConfigEntry.h b/src/Library/Config/ConfigEntry.h index 1b5be9349c6c..5b0624406349 100644 --- a/src/Library/Config/ConfigEntry.h +++ b/src/Library/Config/ConfigEntry.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "Library/Serialization/Serialization.h" @@ -13,7 +14,8 @@ template class ConfigEntry : public AnyConfigEntry { - public: + using base_type = AnyConfigEntry; + public: // NOLINT: why are you complaining? ConfigEntry(const ConfigEntry &other) = delete; // non-copyable ConfigEntry(ConfigEntry &&other) = delete; // non-movable @@ -66,6 +68,15 @@ class ConfigEntry : public AnyConfigEntry { setValue(INT_MAX); } + using base_type::addListener; + + template Listener> + void addListener(void *ctx, Listener listener) { + addListener(ctx, [this, listener = std::move(listener)] { + listener(value()); + }); + } + private: template static Validator wrapValidator(TypedValidator validator) { diff --git a/src/Library/Image/Image.h b/src/Library/Image/Image.h index c8917da3f4b8..babfe651d516 100644 --- a/src/Library/Image/Image.h +++ b/src/Library/Image/Image.h @@ -8,6 +8,7 @@ #include #include "Library/Color/Color.h" +#include "Library/Geometry/Point.h" #include "Library/Geometry/Size.h" #include "Utility/Memory/FreeDeleter.h" @@ -63,6 +64,14 @@ class ImageBase { return const_cast(*this)[y]; } + [[nodiscard]] T &operator[](Pointi point) { + return (*this)[point.y][point.x]; + } + + [[nodiscard]] const T &operator[](Pointi point) const { + return const_cast(*this)[point]; + } + explicit operator bool() const { return static_cast(_pixels.get()); } @@ -73,6 +82,10 @@ class ImageBase { _pixels.reset(); } + void fill(const T &color) { + std::fill_n(pixels().data(), pixels().size(), color); + } + protected: // Directly accessible from derived classes. ssize_t _width = 0; ssize_t _height = 0; @@ -151,6 +164,16 @@ class Image : public detail::ImageBase> { return result; } + /** + * Creates a copy of another image. + * + * @param other Image to copy. + * @return Newly allocated `Image` containing a copy of `other`. + */ + static Image copy(const Image &other) { + return copy(other.width(), other.height(), other.pixels().data()); + } + // The rest is inherited from ImageBase. };