diff options
Diffstat (limited to '')
-rw-r--r-- | src/Generating/GridStructGen.cpp | 13 | ||||
-rw-r--r-- | src/Generating/PieceGenerator.cpp | 287 | ||||
-rw-r--r-- | src/Generating/PieceGenerator.h | 52 | ||||
-rw-r--r-- | src/Generating/Prefab.cpp | 14 | ||||
-rw-r--r-- | src/Generating/Prefab.h | 2 | ||||
-rw-r--r-- | src/Generating/PrefabPiecePool.cpp | 12 | ||||
-rw-r--r-- | src/Generating/VillageGen.cpp | 12 | ||||
-rw-r--r-- | src/StringUtils.h | 6 | ||||
-rw-r--r-- | tests/CMakeLists.txt | 3 | ||||
-rw-r--r-- | tests/PieceRotation/Bindings.h | 15 | ||||
-rw-r--r-- | tests/PieceRotation/CMakeLists.txt | 102 | ||||
-rw-r--r-- | tests/PieceRotation/LuaState_Declaration.inc | 4 | ||||
-rw-r--r-- | tests/PieceRotation/LuaState_Typedefs.inc | 19 | ||||
-rw-r--r-- | tests/PieceRotation/PieceRotationTest.cpp | 170 | ||||
-rw-r--r-- | tests/PieceRotation/Stubs.cpp | 290 |
15 files changed, 952 insertions, 49 deletions
diff --git a/src/Generating/GridStructGen.cpp b/src/Generating/GridStructGen.cpp index 0068783eb..83a98706f 100644 --- a/src/Generating/GridStructGen.cpp +++ b/src/Generating/GridStructGen.cpp @@ -101,12 +101,13 @@ cGridStructGen::cGridStructGen(int a_Seed): void cGridStructGen::SetGeneratorParams(const AStringMap & a_GeneratorParams) { ASSERT(m_Cache.empty()); // No changing the params after chunks are generated - m_GridSizeX = GetStringMapInteger<int>(a_GeneratorParams, "GridSizeX", m_GridSizeX); - m_GridSizeZ = GetStringMapInteger<int>(a_GeneratorParams, "GridSizeZ", m_GridSizeZ); - m_MaxOffsetX = GetStringMapInteger<int>(a_GeneratorParams, "MaxOffsetX", m_MaxOffsetX); - m_MaxOffsetZ = GetStringMapInteger<int>(a_GeneratorParams, "MaxOffsetZ", m_MaxOffsetZ); - m_MaxStructureSizeX = GetStringMapInteger<int>(a_GeneratorParams, "MaxStructureSizeX", m_MaxStructureSizeX); - m_MaxStructureSizeZ = GetStringMapInteger<int>(a_GeneratorParams, "MaxStructureSizeZ", m_MaxStructureSizeZ); + m_GridSizeX = GetStringMapInteger<int> (a_GeneratorParams, "GridSizeX", m_GridSizeX); + m_GridSizeZ = GetStringMapInteger<int> (a_GeneratorParams, "GridSizeZ", m_GridSizeZ); + m_MaxOffsetX = GetStringMapInteger<int> (a_GeneratorParams, "MaxOffsetX", m_MaxOffsetX); + m_MaxOffsetZ = GetStringMapInteger<int> (a_GeneratorParams, "MaxOffsetZ", m_MaxOffsetZ); + m_MaxStructureSizeX = GetStringMapInteger<int> (a_GeneratorParams, "MaxStructureSizeX", m_MaxStructureSizeX); + m_MaxStructureSizeZ = GetStringMapInteger<int> (a_GeneratorParams, "MaxStructureSizeZ", m_MaxStructureSizeZ); + m_MaxCacheSize = GetStringMapInteger<size_t>(a_GeneratorParams, "MaxCacheSize", m_MaxCacheSize); // Silently fix out-of-range parameters: if (m_MaxOffsetX < 1) diff --git a/src/Generating/PieceGenerator.cpp b/src/Generating/PieceGenerator.cpp index 842ac349b..f8ae1d961 100644 --- a/src/Generating/PieceGenerator.cpp +++ b/src/Generating/PieceGenerator.cpp @@ -95,19 +95,19 @@ cPiece::cConnector cPiece::RotateMoveConnector(const cConnector & a_Connector, i case 1: { // 1 CCW rotation: - res.m_Direction = RotateBlockFaceCCW(res.m_Direction); + res.m_Direction = cConnector::RotateDirectionCCW(res.m_Direction); break; } case 2: { // 2 rotations ( = axis flip): - res.m_Direction = MirrorBlockFaceY(res.m_Direction); + res.m_Direction = cConnector::RotateDirection(res.m_Direction); break; } case 3: { // 1 CW rotation: - res.m_Direction = RotateBlockFaceCW(res.m_Direction); + res.m_Direction = cConnector::RotateDirectionCW(res.m_Direction); break; } } @@ -159,7 +159,7 @@ cCuboid cPiece::RotateMoveHitBox(int a_NumCCWRotations, int a_MoveX, int a_MoveY //////////////////////////////////////////////////////////////////////////////// // cPiece::cConnector: -cPiece::cConnector::cConnector(int a_X, int a_Y, int a_Z, int a_Type, eBlockFace a_Direction) : +cPiece::cConnector::cConnector(int a_X, int a_Y, int a_Z, int a_Type, eDirection a_Direction) : m_Pos(a_X, a_Y, a_Z), m_Type(a_Type), m_Direction(a_Direction) @@ -170,7 +170,7 @@ cPiece::cConnector::cConnector(int a_X, int a_Y, int a_Z, int a_Type, eBlockFace -cPiece::cConnector::cConnector(const Vector3i & a_Pos, int a_Type, eBlockFace a_Direction) : +cPiece::cConnector::cConnector(const Vector3i & a_Pos, int a_Type, eDirection a_Direction) : m_Pos(a_Pos), m_Type(a_Type), m_Direction(a_Direction) @@ -181,6 +181,247 @@ cPiece::cConnector::cConnector(const Vector3i & a_Pos, int a_Type, eBlockFace a_ +Vector3i cPiece::cConnector::AddDirection(const Vector3i & a_Pos, eDirection a_Direction) +{ + switch (a_Direction) + { + case dirXM: return Vector3i(a_Pos.x - 1, a_Pos.y, a_Pos.z); + case dirXP: return Vector3i(a_Pos.x + 1, a_Pos.y, a_Pos.z); + case dirYM: return Vector3i(a_Pos.x, a_Pos.y - 1, a_Pos.z); + case dirYP: return Vector3i(a_Pos.x, a_Pos.y + 1, a_Pos.z); + case dirZM: return Vector3i(a_Pos.x, a_Pos.y, a_Pos.z - 1); + case dirZP: return Vector3i(a_Pos.x, a_Pos.y, a_Pos.z + 1); + case dirYM_XM_ZM: return Vector3i(a_Pos.x, a_Pos.y - 1, a_Pos.z); + case dirYM_XM_ZP: return Vector3i(a_Pos.x, a_Pos.y - 1, a_Pos.z); + case dirYM_XP_ZM: return Vector3i(a_Pos.x, a_Pos.y - 1, a_Pos.z); + case dirYM_XP_ZP: return Vector3i(a_Pos.x, a_Pos.y - 1, a_Pos.z); + case dirYP_XM_ZM: return Vector3i(a_Pos.x, a_Pos.y + 1, a_Pos.z); + case dirYP_XM_ZP: return Vector3i(a_Pos.x, a_Pos.y + 1, a_Pos.z); + case dirYP_XP_ZM: return Vector3i(a_Pos.x, a_Pos.y + 1, a_Pos.z); + case dirYP_XP_ZP: return Vector3i(a_Pos.x, a_Pos.y + 1, a_Pos.z); + } + #if !defined(__clang__) + ASSERT(!"Unknown connector direction"); + return a_Pos; + #endif +} + + + + + +const char * cPiece::cConnector::DirectionToString(eDirection a_Direction) +{ + switch (a_Direction) + { + case dirXM: return "x-"; + case dirXP: return "x+"; + case dirYM: return "y-"; + case dirYP: return "y+"; + case dirZM: return "z-"; + case dirZP: return "z+"; + case dirYM_XM_ZM: return "y-x-z-"; + case dirYM_XM_ZP: return "y-x-z+"; + case dirYM_XP_ZM: return "y-x+z-"; + case dirYM_XP_ZP: return "y-x+z+"; + case dirYP_XM_ZM: return "y+x-z-"; + case dirYP_XM_ZP: return "y+x-z+"; + case dirYP_XP_ZM: return "y+x+z-"; + case dirYP_XP_ZP: return "y+x+z+"; + } + #if !defined(__clang__) + ASSERT(!"Unknown connector direction"); + return "<unknown>"; + #endif +} + + + + + +bool cPiece::cConnector::IsValidDirection(int a_Direction) +{ + switch (a_Direction) + { + case dirXM: + case dirXP: + case dirYM: + case dirYP: + case dirZM: + case dirZP: + case dirYM_XM_ZM: + case dirYM_XM_ZP: + case dirYM_XP_ZM: + case dirYM_XP_ZP: + case dirYP_XM_ZM: + case dirYP_XM_ZP: + case dirYP_XP_ZM: + case dirYP_XP_ZP: + { + return true; + } + } + return false; +} + + + + + +cPiece::cConnector::eDirection cPiece::cConnector::RotateDirection(eDirection a_Direction) +{ + // 180-degree rotation: + switch (a_Direction) + { + case dirXM: return dirXP; + case dirXP: return dirXM; + case dirYM: return dirYM; + case dirYP: return dirYP; + case dirZM: return dirZM; + case dirZP: return dirZP; + case dirYM_XM_ZM: return dirYM_XP_ZP; + case dirYM_XM_ZP: return dirYM_XP_ZM; + case dirYM_XP_ZM: return dirYM_XM_ZP; + case dirYM_XP_ZP: return dirYM_XM_ZM; + case dirYP_XM_ZM: return dirYP_XP_ZP; + case dirYP_XM_ZP: return dirYP_XP_ZM; + case dirYP_XP_ZM: return dirYP_XM_ZP; + case dirYP_XP_ZP: return dirYP_XM_ZM; + } + #if !defined(__clang__) + ASSERT(!"Unknown connector direction"); + return a_Direction; + #endif +} + + + + + +cPiece::cConnector::eDirection cPiece::cConnector::RotateDirectionCCW(eDirection a_Direction) +{ + // 90 degrees CCW rotation: + switch (a_Direction) + { + case dirXM: return dirZP; + case dirXP: return dirZM; + case dirYM: return dirYM; + case dirYP: return dirYP; + case dirZM: return dirXM; + case dirZP: return dirXP; + case dirYM_XM_ZM: return dirYM_XM_ZP; + case dirYM_XM_ZP: return dirYM_XP_ZP; + case dirYM_XP_ZM: return dirYM_XM_ZM; + case dirYM_XP_ZP: return dirYM_XP_ZM; + case dirYP_XM_ZM: return dirYP_XM_ZP; + case dirYP_XM_ZP: return dirYP_XP_ZP; + case dirYP_XP_ZM: return dirYP_XM_ZM; + case dirYP_XP_ZP: return dirYP_XP_ZM; + } + #if !defined(__clang__) + ASSERT(!"Unknown connector direction"); + return a_Direction; + #endif +} + + + + + +cPiece::cConnector::eDirection cPiece::cConnector::RotateDirectionCW(eDirection a_Direction) +{ + // 90 degrees CW rotation: + switch (a_Direction) + { + case dirXM: return dirZM; + case dirXP: return dirZP; + case dirYM: return dirYM; + case dirYP: return dirYP; + case dirZM: return dirXP; + case dirZP: return dirXM; + case dirYM_XM_ZM: return dirYM_XP_ZM; + case dirYM_XM_ZP: return dirYM_XM_ZM; + case dirYM_XP_ZM: return dirYM_XP_ZP; + case dirYM_XP_ZP: return dirYM_XM_ZP; + case dirYP_XM_ZM: return dirYP_XP_ZM; + case dirYP_XM_ZP: return dirYP_XM_ZM; + case dirYP_XP_ZM: return dirYP_XP_ZP; + case dirYP_XP_ZP: return dirYP_XM_ZP; + } + #if !defined(__clang__) + ASSERT(!"Unknown connector direction"); + return a_Direction; + #endif +} + + + + + +bool cPiece::cConnector::StringToDirection(const AString & a_Value, eDirection & a_Out) +{ + // First try converting as a number: + int dirInt; + if (StringToInteger(a_Value, dirInt)) + { + if (!IsValidDirection(dirInt)) + { + return false; + } + a_Out = static_cast<eDirection>(dirInt); + return true; + } + + // Compare to string representation: + static const struct + { + const char * m_String; + eDirection m_Value; + } StringDirections[] = + { + {"x-", dirXM}, + {"x+", dirXP}, + {"y-", dirYM}, + {"y+", dirYP}, + {"z-", dirZM}, + {"z+", dirZP}, + {"y-x-z-", dirYM_XM_ZM}, + {"y-x-z+", dirYM_XM_ZP}, + {"y-x+z-", dirYM_XP_ZM}, + {"y-x+z+", dirYM_XP_ZP}, + {"y+x-z-", dirYP_XM_ZM}, + {"y+x-z+", dirYP_XM_ZP}, + {"y+x+z-", dirYP_XP_ZM}, + {"y+x+z+", dirYP_XP_ZP}, + + // Alternate names, with slashes: + {"y-/x-/z-", dirYM_XM_ZM}, + {"y-/x-/z+", dirYM_XM_ZP}, + {"y-/x+/z-", dirYM_XP_ZM}, + {"y-/x+/z+", dirYM_XP_ZP}, + {"y+/x-/z-", dirYP_XM_ZM}, + {"y+/x-/z+", dirYP_XM_ZP}, + {"y+/x+/z-", dirYP_XP_ZM}, + {"y+/x+/z+", dirYP_XP_ZP}, + }; + auto lcValue = StrToLower(a_Value); + for (size_t i = 0; i < ARRAYCOUNT(StringDirections); i++) + { + if (strcmp(lcValue.c_str(), StringDirections[i].m_String) == 0) + { + a_Out = StringDirections[i].m_Value; + return true; + } + } + + // Not understood, failure: + return false; +} + + + + + //////////////////////////////////////////////////////////////////////////////// // cPlacedPiece: @@ -332,26 +573,35 @@ bool cPieceGenerator::TryPlacePieceAtConnector( ) { // Translation of direction - direction -> number of CCW rotations needed: - // You need DirectionRotationTable[rot1][rot2] CCW turns to connect rot1 to rot2 (they are opposite) - static const int DirectionRotationTable[6][6] = + // You need DirectionRotationTable[rot2][rot1] CCW turns to connect rot1 to rot2 (they are opposite) + // -1 if not possible + static const int DirectionRotationTable[14][14] = { - /* YM, YP, ZM, ZP, XM, XP */ - /* YM */ { 0, 0, 0, 0, 0, 0}, - /* YP */ { 0, 0, 0, 0, 0, 0}, - /* ZM */ { 0, 0, 2, 0, 1, 3}, - /* ZP */ { 0, 0, 0, 2, 3, 1}, - /* XM */ { 0, 0, 3, 1, 2, 0}, - /* XP */ { 0, 0, 1, 3, 0, 2}, + /* YM, YP, ZM, ZP, XM, XP, YM-XM-ZM, YM-XM-ZP, YM-XP-ZM, YM-XP-ZP, YP-XM-ZM, YP-XM-ZP, YP-XP-ZM, YP-XP-ZP */ + /* YM */ { 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + /* YP */ {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + /* ZM */ {-1, -1, 2, 0, 1, 3, -1, -1, -1, -1, -1, -1, -1, -1}, + /* ZP */ {-1, -1, 0, 2, 3, 1, -1, -1, -1, -1, -1, -1, -1, -1}, + /* XM */ {-1, -1, 3, 1, 2, 0, -1, -1, -1, -1, -1, -1, -1, -1}, + /* XP */ {-1, -1, 1, 3, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1}, + /* YM-XM-ZM */ {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 3, 1, 2}, + /* YM-XM-ZP */ {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 0, 2, 3}, + /* YM-XP-ZM */ {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 2, 0, 1}, + /* YM-XP-ZP */ {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 1, 3, 0}, + /* YP-XM-ZM */ {-1, -1, -1, -1, -1, -1, 0, 3, 1, 2, -1, -1, -1, -1}, + /* YP-XM-ZP */ {-1, -1, -1, -1, -1, -1, 1, 0, 2, 3, -1, -1, -1, -1}, + /* YP-XP-ZM */ {-1, -1, -1, -1, -1, -1, 3, 2, 0, 1, -1, -1, -1, -1}, + /* YP-XP-ZP */ {-1, -1, -1, -1, -1, -1, 2, 1, 3, 0, -1, -1, -1, -1}, }; // Get a list of available connections: + ASSERT(a_Connector.m_Direction < ARRAYCOUNT(DirectionRotationTable)); const int * RotTable = DirectionRotationTable[a_Connector.m_Direction]; cConnections Connections; int WantedConnectorType = -a_Connector.m_Type; cPieces AvailablePieces = m_PiecePool.GetPiecesWithConnector(WantedConnectorType); Connections.reserve(AvailablePieces.size()); - Vector3i ConnPos = a_Connector.m_Pos; // The position at which the new connector should be placed - 1 block away from the connector - AddFaceDirection(ConnPos.x, ConnPos.y, ConnPos.z, a_Connector.m_Direction); + Vector3i ConnPos = cPiece::cConnector::AddDirection(a_Connector.m_Pos, a_Connector.m_Direction); // The position at which the new connector should be placed - 1 block away from the current connector int WeightTotal = 0; for (cPieces::iterator itrP = AvailablePieces.begin(), endP = AvailablePieces.end(); itrP != endP; ++itrP) { @@ -372,8 +622,9 @@ bool cPieceGenerator::TryPlacePieceAtConnector( continue; } // This is a same-type connector, find out how to rotate to it: + ASSERT(itrC->m_Direction < ARRAYCOUNT(DirectionRotationTable[0])); int NumCCWRotations = RotTable[itrC->m_Direction]; - if (!(*itrP)->CanRotateCCW(NumCCWRotations)) + if ((NumCCWRotations < 0) || !(*itrP)->CanRotateCCW(NumCCWRotations)) { // Doesn't support this rotation continue; @@ -482,7 +733,7 @@ void cPieceGenerator::DebugConnectorPool(const cPieceGenerator::cFreeConnectors idx, itr->m_Connector.m_Pos.x, itr->m_Connector.m_Pos.y, itr->m_Connector.m_Pos.z, itr->m_Connector.m_Type, - BlockFaceToString(itr->m_Connector.m_Direction).c_str(), + cPiece::cConnector::DirectionToString(itr->m_Connector.m_Direction), itr->m_Piece->GetDepth() ); } // for itr - a_ConnectorPool[] diff --git a/src/Generating/PieceGenerator.h b/src/Generating/PieceGenerator.h index 1d0e9deda..1900d9d02 100644 --- a/src/Generating/PieceGenerator.h +++ b/src/Generating/PieceGenerator.h @@ -36,6 +36,27 @@ public: struct cConnector { + enum eDirection + { + // The following values correspond to equivalent eBlockFace values: + dirXM = BLOCK_FACE_XM, // Pointing towards the X- side of the prefab + dirXP = BLOCK_FACE_XP, // Pointing towards the X+ side of the prefab + dirYM = BLOCK_FACE_YM, // Pointing towards the Y- side of the prefab, doesn't change with rotation around the Y axis + dirYP = BLOCK_FACE_YP, // Pointing towards the Y+ side of the prefab, doesn't change with rotation around the Y axis + dirZM = BLOCK_FACE_ZM, // Pointing towards the Z- side of the prefab + dirZP = BLOCK_FACE_ZP, // Pointing towards the Z+ side of the prefab + + // Special kind of the vertical connectors (changes with rotation around the Y axis) + dirYM_XM_ZM = BLOCK_FACE_MAX + 1, // Pointing towards the Y- side of the prefab, conceptually at the X- Z- corner of the block + dirYM_XM_ZP, // Pointing towards the Y- side of the prefab, conceptually at the X- Z+ corner of the block + dirYM_XP_ZM, // Pointing towards the Y- side of the prefab, conceptually at the X+ Z- corner of the block + dirYM_XP_ZP, // Pointing towards the Y- side of the prefab, conceptually at the X+ Z+ corner of the block + dirYP_XM_ZM, // Pointing towards the Y+ side of the prefab, conceptually at the X- Z- corner of the block + dirYP_XM_ZP, // Pointing towards the Y+ side of the prefab, conceptually at the X- Z+ corner of the block + dirYP_XP_ZM, // Pointing towards the Y+ side of the prefab, conceptually at the X+ Z- corner of the block + dirYP_XP_ZP, // Pointing towards the Y+ side of the prefab, conceptually at the X+ Z+ corner of the block + }; + /** Position relative to the piece */ Vector3i m_Pos; @@ -45,10 +66,35 @@ public: /** Direction in which the connector is facing. Will be matched by the opposite direction for the connecting connector. */ - eBlockFace m_Direction; + eDirection m_Direction; + + cConnector(int a_X, int a_Y, int a_Z, int a_Type, eDirection a_Direction); + cConnector(const Vector3i & a_Pos, int a_Type, eDirection a_Direction); + + /** Returns the position of the block that has the specified direction from the specified position. + Similar to ::AddFaceDirection() */ + static Vector3i AddDirection(const Vector3i & a_Pos, eDirection a_Direction); + + /** Returns the string representation of the direction. + For debugging purposes. */ + static const char * DirectionToString(eDirection a_Direction); + + /** Returns true if the specified number corresponds to a valid eDirection. */ + static bool IsValidDirection(int a_Direction); + + /** Returns the direction corresponding to the given direction rotated 180 degrees around the Y axis. */ + static eDirection RotateDirection(eDirection a_Direction); + + /** Returns the direction corresponding to the given direction rotated 90 degrees CCW around the Y axis. */ + static eDirection RotateDirectionCCW(eDirection a_Direction); + + /** Returns the direction corresponding to the given direction rotated 90 degrees CW around the Y axis. */ + static eDirection RotateDirectionCW(eDirection a_Direction); - cConnector(int a_X, int a_Y, int a_Z, int a_Type, eBlockFace a_Direction); - cConnector(const Vector3i & a_Pos, int a_Type, eBlockFace a_Direction); + /** Converts the string representation of a direction into the eDirection enum value. + Returns true if successful, false on failure. + Accepts both numbers and string representations such as "x+" or "Y+X-Z+". */ + static bool StringToDirection(const AString & a_Value, eDirection & a_Out); }; typedef std::vector<cConnector> cConnectors; diff --git a/src/Generating/Prefab.cpp b/src/Generating/Prefab.cpp index e7d9ba711..de2f5f95d 100644 --- a/src/Generating/Prefab.cpp +++ b/src/Generating/Prefab.cpp @@ -299,7 +299,7 @@ void cPrefab::SetDefaultWeight(int a_DefaultWeight) -void cPrefab::AddConnector(int a_RelX, int a_RelY, int a_RelZ, eBlockFace a_Direction, int a_Type) +void cPrefab::AddConnector(int a_RelX, int a_RelY, int a_RelZ, cPiece::cConnector::eDirection a_Direction, int a_Type) { m_Connectors.push_back(cConnector(a_RelX, a_RelY, a_RelZ, a_Type, a_Direction)); } @@ -390,7 +390,7 @@ void cPrefab::ParseConnectors(const char * a_ConnectorsDef) { continue; } - // Split into components: "Type: X, Y, Z: Face": + // Split into components: "Type: X, Y, Z: Direction": AStringVector Defs = StringSplitAndTrim(*itr, ":"); if (Defs.size() != 3) { @@ -404,11 +404,11 @@ void cPrefab::ParseConnectors(const char * a_ConnectorsDef) continue; } - // Check that the BlockFace is within range: - int BlockFace = atoi(Defs[2].c_str()); - if ((BlockFace < 0) || (BlockFace >= 6)) + // Check that the Direction is valid: + cPiece::cConnector::eDirection Direction; + if (!cPiece::cConnector::StringToDirection(Defs[2], Direction)) { - LOGWARNING("Bad prefab Connector Blockface: \"%s\", skipping.", Defs[2].c_str()); + LOGWARNING("Bad prefab Connector direction: \"%s\", skipping.", Defs[2].c_str()); continue; } @@ -416,7 +416,7 @@ void cPrefab::ParseConnectors(const char * a_ConnectorsDef) m_Connectors.push_back(cPiece::cConnector( atoi(Coords[0].c_str()), atoi(Coords[1].c_str()), atoi(Coords[2].c_str()), // Connector pos atoi(Defs[0].c_str()), // Connector type - static_cast<eBlockFace>(BlockFace) + Direction )); } // for itr - Lines[] } diff --git a/src/Generating/Prefab.h b/src/Generating/Prefab.h index bb961f2b3..59b80a8a8 100644 --- a/src/Generating/Prefab.h +++ b/src/Generating/Prefab.h @@ -132,7 +132,7 @@ public: void SetAddWeightIfSame(int a_AddWeightIfSame) { m_AddWeightIfSame = a_AddWeightIfSame; } /** Adds the specified connector to the list of connectors this piece supports. */ - void AddConnector(int a_RelX, int a_RelY, int a_RelZ, eBlockFace a_Direction, int a_Type); + void AddConnector(int a_RelX, int a_RelY, int a_RelZ, cPiece::cConnector::eDirection a_Direction, int a_Type); /** Returns whether the prefab should be moved Y-wise to ground before drawing, rather than staying at the coords governed by the connectors. */ diff --git a/src/Generating/PrefabPiecePool.cpp b/src/Generating/PrefabPiecePool.cpp index 407915e56..9c4e24e92 100644 --- a/src/Generating/PrefabPiecePool.cpp +++ b/src/Generating/PrefabPiecePool.cpp @@ -487,17 +487,23 @@ bool cPrefabPiecePool::ReadConnectorsCubesetVer1( break; } int Type = 0, RelX = 0, RelY = 0, RelZ = 0; - eBlockFace Direction = BLOCK_FACE_NONE; + AString DirectionStr; + cPiece::cConnector::eDirection Direction = cPiece::cConnector::dirYM; if ( !a_LuaState.GetNamedValue("Type", Type) || !a_LuaState.GetNamedValue("RelX", RelX) || !a_LuaState.GetNamedValue("RelY", RelY) || !a_LuaState.GetNamedValue("RelZ", RelZ) || - !a_LuaState.GetNamedValue("Direction", Direction) + !a_LuaState.GetNamedValue("Direction", DirectionStr) || + !cPiece::cConnector::StringToDirection(DirectionStr, Direction) ) { - CONDWARNING(a_LogWarnings, "Piece %s in file %s has a malformed Connector at index %d. Skipping the connector.", a_PieceName.c_str(), a_FileName.c_str(), idx); + CONDWARNING(a_LogWarnings, "Piece %s in file %s has a malformed Connector at index %d ({%d, %d, %d}, type %d, direction %s). Skipping the connector.", + a_PieceName.c_str(), a_FileName.c_str(), idx, RelX, RelY, RelZ, Type, DirectionStr.c_str() + ); res = false; + lua_pop(a_LuaState, 1); // stk: [Connectors] + idx += 1; continue; } a_Prefab->AddConnector(RelX, RelY, RelZ, Direction, Type); diff --git a/src/Generating/VillageGen.cpp b/src/Generating/VillageGen.cpp index ccea3338d..6d5216a16 100644 --- a/src/Generating/VillageGen.cpp +++ b/src/Generating/VillageGen.cpp @@ -64,22 +64,22 @@ public: BA.Create(len, 1, 3, cBlockArea::baTypes | cBlockArea::baMetas); BA.Fill(cBlockArea::baTypes | cBlockArea::baMetas, E_BLOCK_GRAVEL, 0); cPrefab * RoadPiece = new cPrefab(BA, 1); - RoadPiece->AddConnector(0, 0, 1, BLOCK_FACE_XM, -2); - RoadPiece->AddConnector(len - 1, 0, 1, BLOCK_FACE_XP, -2); + RoadPiece->AddConnector(0, 0, 1, cPiece::cConnector::dirXM, -2); + RoadPiece->AddConnector(len - 1, 0, 1, cPiece::cConnector::dirXP, -2); RoadPiece->SetDefaultWeight(100); // Add the road connectors: for (int x = 1; x < len; x += 12) { - RoadPiece->AddConnector(x, 0, 0, BLOCK_FACE_ZM, 2); - RoadPiece->AddConnector(x, 0, 2, BLOCK_FACE_ZP, 2); + RoadPiece->AddConnector(x, 0, 0, cPiece::cConnector::dirZM, 2); + RoadPiece->AddConnector(x, 0, 2, cPiece::cConnector::dirZP, 2); } // Add the buildings connectors: for (int x = 7; x < len; x += 12) { - RoadPiece->AddConnector(x, 0, 0, BLOCK_FACE_ZM, 1); - RoadPiece->AddConnector(x, 0, 2, BLOCK_FACE_ZP, 1); + RoadPiece->AddConnector(x, 0, 0, cPiece::cConnector::dirZM, 1); + RoadPiece->AddConnector(x, 0, 2, cPiece::cConnector::dirZP, 1); } m_AllPieces.push_back(RoadPiece); m_PiecesByConnector[-2].push_back(RoadPiece); diff --git a/src/StringUtils.h b/src/StringUtils.h index e2be2b9c0..620323e70 100644 --- a/src/StringUtils.h +++ b/src/StringUtils.h @@ -220,10 +220,10 @@ bool StringToInteger(const AString & a_str, T & a_Num) -/** Returns an integer from a key-value string map. -Returns a_Default if the key is not present or the value is not an int. */ +/** Returns a number (of any integer type T) from a key-value string map. +Returns a_Default if the key is not present or the value is not a number representable in type T. */ template <typename T> -int GetStringMapInteger(const AStringMap & a_Map, const AString & a_Key, T a_Default) +T GetStringMapInteger(const AStringMap & a_Map, const AString & a_Key, T a_Default) { // Try to locate the key: auto itr = a_Map.find(a_Key); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1010034fd..4b6e64a23 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,8 +4,6 @@ if (CMAKE_BUILD_TYPE STREQUAL "COVERAGE") setup_target_for_coverage("${PROJECT_NAME}_coverage" "ctest" coverage) endif() -# include_directories(${CMAKE_CURRENT_SOURCE_DIR}) - add_subdirectory(BoundingBox) add_subdirectory(ByteBuffer) add_subdirectory(ChunkData) @@ -16,4 +14,5 @@ add_subdirectory(LoadablePieces) add_subdirectory(LuaThreadStress) add_subdirectory(Network) add_subdirectory(OSSupport) +add_subdirectory(PieceRotation) add_subdirectory(SchematicFileSerializer) diff --git a/tests/PieceRotation/Bindings.h b/tests/PieceRotation/Bindings.h new file mode 100644 index 000000000..490830ac3 --- /dev/null +++ b/tests/PieceRotation/Bindings.h @@ -0,0 +1,15 @@ + +// Bindings.h + +// Dummy include file needed for LuaState to compile successfully + + + + +struct lua_State; + +int tolua_AllToLua_open(lua_State * a_LuaState); + + + + diff --git a/tests/PieceRotation/CMakeLists.txt b/tests/PieceRotation/CMakeLists.txt new file mode 100644 index 000000000..53b4f273f --- /dev/null +++ b/tests/PieceRotation/CMakeLists.txt @@ -0,0 +1,102 @@ +enable_testing() + +include_directories(${CMAKE_SOURCE_DIR}/src/) +include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/lib/) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +add_definitions(-DTEST_GLOBALS=1) + +set (SHARED_SRCS + ${CMAKE_SOURCE_DIR}/src/BiomeDef.cpp + ${CMAKE_SOURCE_DIR}/src/BlockArea.cpp + ${CMAKE_SOURCE_DIR}/src/Cuboid.cpp + ${CMAKE_SOURCE_DIR}/src/ChunkData.cpp + ${CMAKE_SOURCE_DIR}/src/StringCompression.cpp + ${CMAKE_SOURCE_DIR}/src/StringUtils.cpp + + ${CMAKE_SOURCE_DIR}/src/Bindings/LuaState.cpp + + ${CMAKE_SOURCE_DIR}/src/Generating/ChunkDesc.cpp + ${CMAKE_SOURCE_DIR}/src/Generating/PieceGenerator.cpp + ${CMAKE_SOURCE_DIR}/src/Generating/Prefab.cpp + ${CMAKE_SOURCE_DIR}/src/Generating/PrefabPiecePool.cpp + ${CMAKE_SOURCE_DIR}/src/Generating/VerticalLimit.cpp + ${CMAKE_SOURCE_DIR}/src/Generating/VerticalStrategy.cpp + + ${CMAKE_SOURCE_DIR}/src/Noise/Noise.cpp + + ${CMAKE_SOURCE_DIR}/src/OSSupport/CriticalSection.cpp + ${CMAKE_SOURCE_DIR}/src/OSSupport/Event.cpp + ${CMAKE_SOURCE_DIR}/src/OSSupport/File.cpp + ${CMAKE_SOURCE_DIR}/src/OSSupport/GZipFile.cpp + ${CMAKE_SOURCE_DIR}/src/OSSupport/StackTrace.cpp + + ${CMAKE_SOURCE_DIR}/src/WorldStorage/FastNBT.cpp + ${CMAKE_SOURCE_DIR}/src/WorldStorage/SchematicFileSerializer.cpp +) + +set (SHARED_HDRS + ${CMAKE_SOURCE_DIR}/src/BiomeDef.h + ${CMAKE_SOURCE_DIR}/src/BlockArea.h + ${CMAKE_SOURCE_DIR}/src/Cuboid.h + ${CMAKE_SOURCE_DIR}/src/ChunkData.h + ${CMAKE_SOURCE_DIR}/src/Globals.h + ${CMAKE_SOURCE_DIR}/src/StringCompression.h + ${CMAKE_SOURCE_DIR}/src/StringUtils.h + + ${CMAKE_SOURCE_DIR}/src/Bindings/LuaState.h + + ${CMAKE_SOURCE_DIR}/src/Generating/ChunkDesc.h + ${CMAKE_SOURCE_DIR}/src/Generating/PieceGenerator.h + ${CMAKE_SOURCE_DIR}/src/Generating/Prefab.h + ${CMAKE_SOURCE_DIR}/src/Generating/PrefabPiecePool.h + ${CMAKE_SOURCE_DIR}/src/Generating/VerticalLimit.h + ${CMAKE_SOURCE_DIR}/src/Generating/VerticalStrategy.h + + ${CMAKE_SOURCE_DIR}/src/Noise/Noise.h + + ${CMAKE_SOURCE_DIR}/src/OSSupport/CriticalSection.h + ${CMAKE_SOURCE_DIR}/src/OSSupport/Event.h + ${CMAKE_SOURCE_DIR}/src/OSSupport/File.h + ${CMAKE_SOURCE_DIR}/src/OSSupport/GZipFile.h + ${CMAKE_SOURCE_DIR}/src/OSSupport/StackTrace.h + + ${CMAKE_SOURCE_DIR}/src/WorldStorage/FastNBT.h + ${CMAKE_SOURCE_DIR}/src/WorldStorage/SchematicFileSerializer.h +) + +set (SRCS + PieceRotationTest.cpp + Stubs.cpp + LuaState_Typedefs.inc + LuaState_Declaration.inc + Bindings.h +) + + +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + add_flags_cxx("-Wno-error=global-constructors") + add_flags_cxx("-Wno-error=switch-enum") +endif() + + +if (MSVC) + # Add the MSVC-specific LeakFinder sources: + list (APPEND SHARED_SRCS ${CMAKE_SOURCE_DIR}/src/LeakFinder.cpp ${CMAKE_SOURCE_DIR}/src/StackWalker.cpp) + list (APPEND SHARED_HDRS ${CMAKE_SOURCE_DIR}/src/LeakFinder.h ${CMAKE_SOURCE_DIR}/src/StackWalker.h) +endif() + +source_group("Shared" FILES ${SHARED_SRCS} ${SHARED_HDRS}) +source_group("Sources" FILES ${SRCS}) +add_executable(PieceRotation ${SRCS} ${SHARED_SRCS} ${SHARED_HDRS}) +target_link_libraries(PieceRotation tolualib zlib) +add_test(NAME PieceRotation-test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND PieceRotation) + + + + +# Put the projects into solution folders (MSVC): +set_target_properties( + PieceRotation + PROPERTIES FOLDER Tests +) diff --git a/tests/PieceRotation/LuaState_Declaration.inc b/tests/PieceRotation/LuaState_Declaration.inc new file mode 100644 index 000000000..4019b26c6 --- /dev/null +++ b/tests/PieceRotation/LuaState_Declaration.inc @@ -0,0 +1,4 @@ + +// LuaState_Declaration.inc + +// Dummy include file needed for LuaState to compile successfully diff --git a/tests/PieceRotation/LuaState_Typedefs.inc b/tests/PieceRotation/LuaState_Typedefs.inc new file mode 100644 index 000000000..5eba7c6f8 --- /dev/null +++ b/tests/PieceRotation/LuaState_Typedefs.inc @@ -0,0 +1,19 @@ + +// LuaState_Typedefs.inc + +// Dummy include file needed for LuaState to compile successfully + + + + + +// Forward-declare classes that are used in the API but never called: +struct HTTPRequest; +struct HTTPTemplateRequest; +class cPluginLua; +class cBoundingBox; +template <typename T> class cItemCallback; +class cEntity; + + + diff --git a/tests/PieceRotation/PieceRotationTest.cpp b/tests/PieceRotation/PieceRotationTest.cpp new file mode 100644 index 000000000..3e325b54c --- /dev/null +++ b/tests/PieceRotation/PieceRotationTest.cpp @@ -0,0 +1,170 @@ +// PieceRotation.cpp + +// Implements the tests for cPiece::cConnector::eDirection rotations + +#include "Globals.h" +#include <exception> +#include <map> +#include "Generating/PieceGenerator.h" + + + + + +static cPiece::cConnector::eDirection g_AllDirections[] = +{ + cPiece::cConnector::dirXM, + cPiece::cConnector::dirXP, + cPiece::cConnector::dirYM, + cPiece::cConnector::dirYP, + cPiece::cConnector::dirZM, + cPiece::cConnector::dirZP, + cPiece::cConnector::dirYM_XM_ZM, + cPiece::cConnector::dirYM_XM_ZP, + cPiece::cConnector::dirYM_XP_ZM, + cPiece::cConnector::dirYM_XP_ZP, + cPiece::cConnector::dirYP_XM_ZM, + cPiece::cConnector::dirYP_XM_ZP, + cPiece::cConnector::dirYP_XP_ZM, + cPiece::cConnector::dirYP_XP_ZP, +}; + + + + + +class cTestFailure: public std::exception +{ +public: + cTestFailure(const char * a_Expression, const char * a_FileName, const int a_Line): + std::exception(), + m_Expression(a_Expression), + m_FileName(a_FileName), + m_Line(a_Line) + { + } + + const std::string m_Expression; + const std::string m_FileName; + const int m_Line; +}; + + + + + +#define EXPECT(X) if (!(X)) \ + { \ + ASSERT(X); \ + throw cTestFailure(#X, __FILE__, __LINE__); \ + } + + + + + +/** Tests that rotating a direction in one way and then the opposite way produces the original direction. */ +static void testBackAndForth() +{ + for (size_t i = 0; i < ARRAYCOUNT(g_AllDirections); ++i) + { + auto rotated = cPiece::cConnector::RotateDirectionCW(g_AllDirections[i]); + auto back = cPiece::cConnector::RotateDirectionCCW(rotated); + EXPECT(back == g_AllDirections[i]); + } + for (size_t i = 0; i < ARRAYCOUNT(g_AllDirections); ++i) + { + auto rotated = cPiece::cConnector::RotateDirectionCCW(g_AllDirections[i]); + auto back = cPiece::cConnector::RotateDirectionCW(rotated); + EXPECT(back == g_AllDirections[i]); + } +} + + + + + +/** Tests rotating the direction 360 degrees. */ +static void testFullRotation() +{ + // Rotate 90 degrees CCW four times: + for (size_t i = 0; i < ARRAYCOUNT(g_AllDirections); ++i) + { + auto d = g_AllDirections[i]; + for (int j = 0; j < 4; ++j) + { + d = cPiece::cConnector::RotateDirectionCCW(d); + } + EXPECT(d == g_AllDirections[i]); + } + + // Rotate 90 degrees CW four times: + for (size_t i = 0; i < ARRAYCOUNT(g_AllDirections); ++i) + { + auto d = g_AllDirections[i]; + for (int j = 0; j < 4; ++j) + { + d = cPiece::cConnector::RotateDirectionCW(d); + } + EXPECT(d == g_AllDirections[i]); + } + + // Rotate 180 degrees twice: + for (size_t i = 0; i < ARRAYCOUNT(g_AllDirections); ++i) + { + auto d = g_AllDirections[i]; + d = cPiece::cConnector::RotateDirection(d); + d = cPiece::cConnector::RotateDirection(d); + EXPECT(d == g_AllDirections[i]); + } +} + + + + + +/** Tests that no two values are rotated onto the same destination value. */ +template <class Fn> +static void testPermutation(Fn & a_Fn) +{ + std::map<cPiece::cConnector::eDirection, int> numDest; + for (size_t i = 0; i < ARRAYCOUNT(g_AllDirections); ++i) + { + auto d = a_Fn(g_AllDirections[i]); + EXPECT(numDest[d] == 0); + numDest[d] = 1; + } +} + + + + + +int main(void) +{ + LOG("PieceRotation test starting."); + try + { + testBackAndForth(); + testFullRotation(); + testPermutation(cPiece::cConnector::RotateDirection); + testPermutation(cPiece::cConnector::RotateDirectionCW); + testPermutation(cPiece::cConnector::RotateDirectionCCW); + } + catch (const cTestFailure & f) + { + LOGERROR("Test failed:\tExpression: %s\n\tFile: %s (%d)", f.m_Expression.c_str(), f.m_FileName.c_str(), f.m_Line); + return 1; + } + catch (const std::exception & exc) + { + LOGERROR("Exception caught: %s", exc.what()); + return 2; + } + LOG("PieceRotation test succeeded."); + return 0; +} + + + + diff --git a/tests/PieceRotation/Stubs.cpp b/tests/PieceRotation/Stubs.cpp new file mode 100644 index 000000000..0ad3eb365 --- /dev/null +++ b/tests/PieceRotation/Stubs.cpp @@ -0,0 +1,290 @@ + +// Stubs.cpp + +// Implements stubs of various Cuberite methods that are needed for linking but not for runtime +// This is required so that we don't bring in the entire Cuberite via dependencies + +#include "Globals.h" +#include "BlockInfo.h" +#include "Bindings.h" +#include "Bindings/DeprecatedBindings.h" +#include "Bindings/LuaJson.h" +#include "Bindings/ManualBindings.h" +#include "BlockEntities/BlockEntity.h" +#include "Blocks/BlockHandler.h" +#include "Generating/ChunkDesc.h" +#include "DeadlockDetect.h" + + + + + +// fwd: +struct lua_State; + + + + + +// Prototypes, needed by clang: +extern "C" int luaopen_lsqlite3(lua_State * a_LuaState); +extern "C" int luaopen_lxp(lua_State * a_LuaState); + + + + + +void cManualBindings::Bind(lua_State * a_LuaState) +{ +} + + + + + +void DeprecatedBindings::Bind(lua_State * a_LuaState) +{ +} + + + + + +void cLuaJson::Bind(cLuaState & a_LuaState) +{ +} + + + + + +int tolua_AllToLua_open(lua_State * a_LuaState) +{ + return 0; +} + + + + + +extern "C" int luaopen_lsqlite3(lua_State * a_LuaState) +{ + return 0; +} + + + + + +extern "C" int luaopen_lxp(lua_State * a_LuaState) +{ + return 0; +} + + + + + +cBlockInfo::~cBlockInfo() +{ +} + + + + + +void cBlockInfo::Initialize(cBlockInfo::cBlockInfoArray & a_BlockInfos) +{ + // The piece-loading code uses the handlers for rotations, so we need valid handlers + // Insert dummy handlers: + for (size_t i = 0; i < ARRAYCOUNT(a_BlockInfos); i++) + { + a_BlockInfos[i].m_Handler = new cBlockHandler(static_cast<BLOCKTYPE>(i)); + } +} + + + + + +cBlockHandler::cBlockHandler(BLOCKTYPE a_BlockType) +{ +} + + + + + +bool cBlockHandler::GetPlacementBlockTypeMeta( + cChunkInterface & a_ChunkInterface, cPlayer * a_Player, + int a_BlockX, int a_BlockY, int a_BlockZ, eBlockFace a_BlockFace, + int a_CursorX, int a_CursorY, int a_CursorZ, + BLOCKTYPE & a_BlockType, NIBBLETYPE & a_BlockMeta +) +{ + return true; +} + + + + + +void cBlockHandler::OnUpdate(cChunkInterface & cChunkInterface, cWorldInterface & a_WorldInterface, cBlockPluginInterface & a_PluginInterface, cChunk & a_Chunk, int a_BlockX, int a_BlockY, int a_BlockZ) +{ +} + + + + + +void cBlockHandler::OnPlacedByPlayer(cChunkInterface & a_ChunkInterface, cWorldInterface & a_WorldInterface, cPlayer * a_Player, const sSetBlock & a_BlockChange) +{ +} + + + + + +void cBlockHandler::OnDestroyedByPlayer(cChunkInterface & a_ChunkInterface, cWorldInterface & a_WorldInterface, cPlayer * a_Player, int a_BlockX, int a_BlockY, int a_BlockZ) +{ +} + + + + + +void cBlockHandler::OnPlaced(cChunkInterface & a_ChunkInterface, cWorldInterface & a_WorldInterface, int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta) +{ +} + + + + + +void cBlockHandler::OnDestroyed(cChunkInterface & a_ChunkInterface, cWorldInterface & a_WorldInterface, int a_BlockX, int a_BlockY, int a_BlockZ) +{ +} + + + + + +void cBlockHandler::NeighborChanged(cChunkInterface & a_ChunkInterface, int a_BlockX, int a_BlockY, int a_BlockZ, eBlockFace a_WhichNeighbor) +{ +} + + + + + +void cBlockHandler::ConvertToPickups(cItems & a_Pickups, NIBBLETYPE a_BlockMeta) +{ +} + + + + + +void cBlockHandler::DropBlock(cChunkInterface & a_ChunkInterface, cWorldInterface & a_WorldInterface, cBlockPluginInterface & a_BlockPluginInterface, cEntity * a_Digger, int a_BlockX, int a_BlockY, int a_BlockZ, bool a_CanDrop) +{ +} + + + + + +bool cBlockHandler::CanBeAt(cChunkInterface & a_ChunkInterface, int a_BlockX, int a_BlockY, int a_BlockZ, const cChunk & a_Chunk) +{ + return true; +} + + + + + +bool cBlockHandler::IsUseable() +{ + return false; +} + + + + + +bool cBlockHandler::IsClickedThrough(void) +{ + return false; +} + + + + + +bool cBlockHandler::DoesIgnoreBuildCollision(void) +{ + return (m_BlockType == E_BLOCK_AIR); +} + + + + + +bool cBlockHandler::DoesDropOnUnsuitable(void) +{ + return true; +} + + + + + +void cBlockHandler::Check(cChunkInterface & a_ChunkInterface, cBlockPluginInterface & a_PluginInterface, int a_RelX, int a_RelY, int a_RelZ, cChunk & a_Chunk) +{ +} + + + + + +ColourID cBlockHandler::GetMapBaseColourID(NIBBLETYPE a_Meta) +{ + return 0; +} + + + + + +bool cBlockHandler::IsInsideBlock(const Vector3d & a_Position, const BLOCKTYPE a_BlockType, const NIBBLETYPE a_BlockMeta) +{ + return true; +} + + + + + +cBlockEntity * cBlockEntity::CreateByBlockType(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) +{ + return nullptr; +} + + + + + +void cDeadlockDetect::TrackCriticalSection(cCriticalSection & a_CS, const AString & a_Name) +{ +} + + + + + +void cDeadlockDetect::UntrackCriticalSection(cCriticalSection & a_CS) +{ +} + + + + |