1 /***************************************************************************
2  *   Copyright (C) 2009 by Andrey Afletdinov <fheroes2@gmail.com>          *
3  *                                                                         *
4  *   Part of the Free Heroes2 Engine:                                      *
5  *   http://sourceforge.net/projects/fheroes2                              *
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or modify  *
8  *   it under the terms of the GNU General Public License as published by  *
9  *   the Free Software Foundation; either version 2 of the License, or     *
10  *   (at your option) any later version.                                   *
11  *                                                                         *
12  *   This program is distributed in the hope that it will be useful,       *
13  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
14  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
15  *   GNU General Public License for more details.                          *
16  *                                                                         *
17  *   You should have received a copy of the GNU General Public License     *
18  *   along with this program; if not, write to the                         *
19  *   Free Software Foundation, Inc.,                                       *
20  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
21  ***************************************************************************/
22 
23 #include <algorithm>
24 #include <cassert>
25 
26 #include "ai.h"
27 #include "difficulty.h"
28 #include "game.h"
29 #include "icn.h"
30 #include "kingdom.h"
31 #include "logging.h"
32 #include "maps.h"
33 #include "maps_tiles.h"
34 #include "race.h"
35 #include "serialize.h"
36 #include "translations.h"
37 #include "world.h"
38 
39 namespace
40 {
getTileToClearIndicies(const int32_t tileIndex,int scouteValue,const int playerColor)41     std::vector<int32_t> getTileToClearIndicies( const int32_t tileIndex, int scouteValue, const int playerColor )
42     {
43         std::vector<int32_t> indicies;
44 
45         if ( scouteValue <= 0 || !Maps::isValidAbsIndex( tileIndex ) ) {
46             return indicies;
47         }
48 
49         const fheroes2::Point center = Maps::GetPoint( tileIndex );
50 
51         // AI is cheating!
52         const bool isAIPlayer = world.GetKingdom( playerColor ).isControlAI();
53         if ( isAIPlayer ) {
54             scouteValue += Difficulty::GetScoutingBonus( Game::getDifficulty() );
55         }
56 
57         const int revealRadiusSquared = scouteValue * scouteValue + 4; // constant factor for "backwards compatibility"
58         for ( int32_t y = center.y - scouteValue; y <= center.y + scouteValue; ++y ) {
59             if ( y < 0 || y >= world.h() )
60                 continue;
61 
62             for ( int32_t x = center.x - scouteValue; x <= center.x + scouteValue; ++x ) {
63                 if ( x < 0 || x >= world.w() )
64                     continue;
65 
66                 const int32_t dx = x - center.x;
67                 const int32_t dy = y - center.y;
68                 if ( revealRadiusSquared >= dx * dx + dy * dy ) {
69                     indicies.emplace_back( Maps::GetIndexFromAbsPoint( x, y ) );
70                 }
71             }
72         }
73 
74         return indicies;
75     }
76 
MapsIndexesFilteredObject(const Maps::Indexes & indexes,const MP2::MapObjectType objectType,const bool ignoreHeroes=true)77     Maps::Indexes MapsIndexesFilteredObject( const Maps::Indexes & indexes, const MP2::MapObjectType objectType, const bool ignoreHeroes = true )
78     {
79         Maps::Indexes result;
80         for ( size_t idx = 0; idx < indexes.size(); ++idx ) {
81             if ( world.GetTiles( indexes[idx] ).GetObject( !ignoreHeroes ) == objectType ) {
82                 result.push_back( indexes[idx] );
83             }
84         }
85         return result;
86     }
87 
MapsIndexesObject(const MP2::MapObjectType objectType,const bool ignoreHeroes=true)88     Maps::Indexes MapsIndexesObject( const MP2::MapObjectType objectType, const bool ignoreHeroes = true )
89     {
90         Maps::Indexes result;
91         const int32_t size = static_cast<int32_t>( world.getSize() );
92         for ( int32_t idx = 0; idx < size; ++idx ) {
93             if ( world.GetTiles( idx ).GetObject( !ignoreHeroes ) == objectType ) {
94                 result.push_back( idx );
95             }
96         }
97         return result;
98     }
99 }
100 
101 struct ComparisonDistance
102 {
ComparisonDistanceComparisonDistance103     explicit ComparisonDistance( const int32_t index )
104         : centerPoint( Maps::GetPoint( index ) )
105     {}
106 
107     ComparisonDistance() = delete;
108 
operator ()ComparisonDistance109     bool operator()( const int32_t index1, const int32_t index2 ) const
110     {
111         const fheroes2::Point point1( Maps::GetPoint( index1 ) );
112         const fheroes2::Point point2( Maps::GetPoint( index2 ) );
113 
114         const int32_t diffX1 = std::abs( centerPoint.x - point1.x );
115         const int32_t diffY1 = std::abs( centerPoint.y - point1.y );
116         const int32_t diffX2 = std::abs( centerPoint.x - point2.x );
117         const int32_t diffY2 = std::abs( centerPoint.y - point2.y );
118 
119         return ( diffX1 * diffX1 + diffY1 * diffY1 ) < ( diffX2 * diffX2 + diffY2 * diffY2 );
120     }
121 
122     const fheroes2::Point centerPoint;
123 };
124 
SizeString(int s)125 const char * Maps::SizeString( int s )
126 {
127     switch ( s ) {
128     case SMALL:
129         return _( "maps|Small" );
130     case MEDIUM:
131         return _( "maps|Medium" );
132     case LARGE:
133         return _( "maps|Large" );
134     case XLARGE:
135         return _( "maps|Extra Large" );
136     default:
137         break;
138     }
139 
140     return _( "maps|Custom Size" );
141 }
142 
GetMinesName(int type)143 const char * Maps::GetMinesName( int type )
144 {
145     switch ( type ) {
146     case Resource::ORE:
147         return _( "Ore Mine" );
148     case Resource::SULFUR:
149         return _( "Sulfur Mine" );
150     case Resource::CRYSTAL:
151         return _( "Crystal Mine" );
152     case Resource::GEMS:
153         return _( "Gems Mine" );
154     case Resource::GOLD:
155         return _( "Gold Mine" );
156     default:
157         break;
158     }
159 
160     return _( "Mine" );
161 }
162 
GetDirection(int from,int to)163 int Maps::GetDirection( int from, int to )
164 {
165     if ( from == to )
166         return Direction::CENTER;
167 
168     const int diff = to - from;
169     const int width = world.w();
170 
171     if ( diff == ( -width - 1 ) ) {
172         return Direction::TOP_LEFT;
173     }
174     else if ( diff == -width ) {
175         return Direction::TOP;
176     }
177     else if ( diff == ( -width + 1 ) ) {
178         return Direction::TOP_RIGHT;
179     }
180     else if ( diff == -1 ) {
181         return Direction::LEFT;
182     }
183     else if ( diff == 1 ) {
184         return Direction::RIGHT;
185     }
186     else if ( diff == width - 1 ) {
187         return Direction::BOTTOM_LEFT;
188     }
189     else if ( diff == width ) {
190         return Direction::BOTTOM;
191     }
192     else if ( diff == width + 1 ) {
193         return Direction::BOTTOM_RIGHT;
194     }
195 
196     return Direction::UNKNOWN;
197 }
198 
GetDirectionIndex(int32_t from,int vector)199 int32_t Maps::GetDirectionIndex( int32_t from, int vector )
200 {
201     switch ( vector ) {
202     case Direction::TOP:
203         return from - world.w();
204     case Direction::TOP_RIGHT:
205         return from - world.w() + 1;
206     case Direction::RIGHT:
207         return from + 1;
208     case Direction::BOTTOM_RIGHT:
209         return from + world.w() + 1;
210     case Direction::BOTTOM:
211         return from + world.w();
212     case Direction::BOTTOM_LEFT:
213         return from + world.w() - 1;
214     case Direction::LEFT:
215         return from - 1;
216     case Direction::TOP_LEFT:
217         return from - world.w() - 1;
218     default:
219         break;
220     }
221 
222     return -1;
223 }
224 
225 // check bound
isValidDirection(int32_t from,int vector)226 bool Maps::isValidDirection( int32_t from, int vector )
227 {
228     const int32_t width = world.w();
229 
230     switch ( vector ) {
231     case Direction::TOP:
232         return ( from >= width );
233     case Direction::RIGHT:
234         return ( ( from % width ) < ( width - 1 ) );
235     case Direction::BOTTOM:
236         return ( from < width * ( world.h() - 1 ) );
237     case Direction::LEFT:
238         return ( from % width ) != 0;
239 
240     case Direction::TOP_RIGHT:
241         return ( from >= width ) && ( ( from % width ) < ( width - 1 ) );
242 
243     case Direction::BOTTOM_RIGHT:
244         return ( from < width * ( world.h() - 1 ) ) && ( ( from % width ) < ( width - 1 ) );
245 
246     case Direction::BOTTOM_LEFT:
247         return ( from < width * ( world.h() - 1 ) ) && ( from % width );
248 
249     case Direction::TOP_LEFT:
250         return ( from >= width ) && ( from % width );
251 
252     default:
253         break;
254     }
255 
256     return false;
257 }
258 
GetPoint(const int32_t index)259 fheroes2::Point Maps::GetPoint( const int32_t index )
260 {
261     return fheroes2::Point( index % world.w(), index / world.w() );
262 }
263 
isValidAbsIndex(const int32_t index)264 bool Maps::isValidAbsIndex( const int32_t index )
265 {
266     return 0 <= index && index < world.w() * world.h();
267 }
268 
isValidAbsPoint(const int32_t x,const int32_t y)269 bool Maps::isValidAbsPoint( const int32_t x, const int32_t y )
270 {
271     return 0 <= x && world.w() > x && 0 <= y && world.h() > y;
272 }
273 
GetIndexFromAbsPoint(const fheroes2::Point & mp)274 int32_t Maps::GetIndexFromAbsPoint( const fheroes2::Point & mp )
275 {
276     if ( mp.x < 0 || mp.y < 0 ) {
277         return -1;
278     }
279 
280     return mp.y * world.w() + mp.x;
281 }
282 
GetIndexFromAbsPoint(const int32_t x,const int32_t y)283 int32_t Maps::GetIndexFromAbsPoint( const int32_t x, const int32_t y )
284 {
285     if ( x < 0 || y < 0 ) {
286         return -1;
287     }
288 
289     return y * world.w() + x;
290 }
291 
getAroundIndexes(const int32_t tileIndex,const int32_t maxDistanceFromTile)292 Maps::Indexes Maps::getAroundIndexes( const int32_t tileIndex, const int32_t maxDistanceFromTile /* = 1 */ )
293 {
294     Indexes results;
295 
296     if ( !isValidAbsIndex( tileIndex ) || maxDistanceFromTile <= 0 ) {
297         return results;
298     }
299 
300     results.reserve( ( maxDistanceFromTile * 2 + 1 ) * ( maxDistanceFromTile * 2 + 1 ) - 1 );
301 
302     assert( world.w() > 0 );
303 
304     const int32_t centerX = tileIndex % world.w();
305     const int32_t centerY = tileIndex / world.w();
306 
307     for ( int32_t y = -maxDistanceFromTile; y <= maxDistanceFromTile; ++y ) {
308         for ( int32_t x = -maxDistanceFromTile; x <= maxDistanceFromTile; ++x ) {
309             // the central tile is not included
310             if ( x == 0 && y == 0 ) {
311                 continue;
312             }
313 
314             const int32_t tileX = centerX + x;
315             const int32_t tileY = centerY + y;
316 
317             if ( isValidAbsPoint( tileX, tileY ) ) {
318                 results.push_back( tileY * world.w() + tileX );
319             }
320         }
321     }
322 
323     return results;
324 }
325 
ClearFog(const int32_t tileIndex,const int scouteValue,const int playerColor)326 void Maps::ClearFog( const int32_t tileIndex, const int scouteValue, const int playerColor )
327 {
328     const std::vector<int32_t> tileIndicies = getTileToClearIndicies( tileIndex, scouteValue, playerColor );
329     if ( tileIndicies.empty() ) {
330         // Nothing to uncover.
331         return;
332     }
333 
334     const bool isAIPlayer = world.GetKingdom( playerColor ).isControlAI();
335     const int alliedColors = Players::GetPlayerFriends( playerColor );
336 
337     for ( const int32_t index : tileIndicies ) {
338         Maps::Tiles & tile = world.GetTiles( index );
339         if ( isAIPlayer && tile.isFog( playerColor ) ) {
340             AI::Get().revealFog( tile );
341         }
342 
343         tile.ClearFog( alliedColors );
344     }
345 }
346 
getFogTileCountToBeRevealed(const int32_t tileIndex,const int scouteValue,const int playerColor)347 int32_t Maps::getFogTileCountToBeRevealed( const int32_t tileIndex, const int scouteValue, const int playerColor )
348 {
349     const std::vector<int32_t> tileIndicies = getTileToClearIndicies( tileIndex, scouteValue, playerColor );
350 
351     int32_t tileCount = 0;
352 
353     for ( const int32_t index : tileIndicies ) {
354         const Maps::Tiles & tile = world.GetTiles( index );
355         if ( tile.isFog( playerColor ) ) {
356             ++tileCount;
357         }
358     }
359 
360     return tileCount;
361 }
362 
ScanAroundObject(const int32_t center,const MP2::MapObjectType objectType,const bool ignoreHeroes)363 Maps::Indexes Maps::ScanAroundObject( const int32_t center, const MP2::MapObjectType objectType, const bool ignoreHeroes )
364 {
365     Indexes results = getAroundIndexes( center );
366     return MapsIndexesFilteredObject( results, objectType, ignoreHeroes );
367 }
368 
GetFreeIndexesAroundTile(const int32_t center)369 Maps::Indexes Maps::GetFreeIndexesAroundTile( const int32_t center )
370 {
371     Indexes results = getAroundIndexes( center );
372     results.erase( std::remove_if( results.begin(), results.end(), []( const int32_t tile ) { return !world.GetTiles( tile ).isClearGround(); } ), results.end() );
373     return results;
374 }
375 
ScanAroundObject(const int32_t center,const MP2::MapObjectType objectType)376 Maps::Indexes Maps::ScanAroundObject( const int32_t center, const MP2::MapObjectType objectType )
377 {
378     Indexes results = getAroundIndexes( center );
379     return MapsIndexesFilteredObject( results, objectType );
380 }
381 
ScanAroundObjectWithDistance(const int32_t center,const uint32_t dist,const MP2::MapObjectType objectType)382 Maps::Indexes Maps::ScanAroundObjectWithDistance( const int32_t center, const uint32_t dist, const MP2::MapObjectType objectType )
383 {
384     Indexes results = getAroundIndexes( center, dist );
385     std::sort( results.begin(), results.end(), ComparisonDistance( center ) );
386     return MapsIndexesFilteredObject( results, objectType );
387 }
388 
GetObjectPositions(const MP2::MapObjectType objectType,bool ignoreHeroes)389 Maps::Indexes Maps::GetObjectPositions( const MP2::MapObjectType objectType, bool ignoreHeroes )
390 {
391     return MapsIndexesObject( objectType, ignoreHeroes );
392 }
393 
GetObjectPositions(int32_t center,const MP2::MapObjectType objectType,bool ignoreHeroes)394 Maps::Indexes Maps::GetObjectPositions( int32_t center, const MP2::MapObjectType objectType, bool ignoreHeroes )
395 {
396     Indexes results = MapsIndexesObject( objectType, ignoreHeroes );
397     std::sort( results.begin(), results.end(), ComparisonDistance( center ) );
398     return results;
399 }
400 
MapsTileIsUnderProtection(int32_t from,int32_t index)401 bool MapsTileIsUnderProtection( int32_t from, int32_t index ) /* from: center, index: monster */
402 {
403     const Maps::Tiles & tile1 = world.GetTiles( from );
404     const Maps::Tiles & tile2 = world.GetTiles( index );
405 
406     if ( !MP2::isPickupObject( tile1.GetObject() ) && tile2.GetObject() == MP2::OBJ_MONSTER && tile1.isWater() == tile2.isWater() ) {
407         const int monsterDirection = Maps::GetDirection( index, from );
408         // if monster can attack to
409         if ( ( tile2.GetPassable() & monsterDirection ) && ( tile1.GetPassable() & Maps::GetDirection( from, index ) ) )
410             return true;
411 
412         // h2 specific monster attack: BOTTOM_LEFT impassable!
413         if ( Direction::BOTTOM_LEFT == monsterDirection && ( Direction::LEFT & tile2.GetPassable() ) && ( Direction::TOP & tile1.GetPassable() ) )
414             return true;
415 
416         // h2 specific monster attack: BOTTOM_RIGHT impassable!
417         if ( Direction::BOTTOM_RIGHT == monsterDirection && ( Direction::RIGHT & tile2.GetPassable() ) && ( Direction::TOP & tile1.GetPassable() ) )
418             return true;
419     }
420 
421     return false;
422 }
423 
TileIsUnderProtection(int32_t center)424 bool Maps::TileIsUnderProtection( int32_t center )
425 {
426     return MP2::OBJ_MONSTER == world.GetTiles( center ).GetObject() ? true : !GetTilesUnderProtection( center ).empty();
427 }
428 
GetTilesUnderProtection(int32_t center)429 Maps::Indexes Maps::GetTilesUnderProtection( int32_t center )
430 {
431     Indexes result;
432     if ( !isValidAbsIndex( center ) )
433         return result;
434 
435     result.reserve( 9 );
436     const int width = world.w();
437     const int x = center % width;
438     const int y = center / width;
439 
440     auto validateAndInsert = [&result, &center]( const int index ) {
441         if ( MapsTileIsUnderProtection( center, index ) )
442             result.push_back( index );
443     };
444 
445     if ( y > 0 ) {
446         if ( x > 0 )
447             validateAndInsert( center - width - 1 );
448 
449         validateAndInsert( center - width );
450 
451         if ( x < width - 1 )
452             validateAndInsert( center - width + 1 );
453     }
454 
455     if ( x > 0 )
456         validateAndInsert( center - 1 );
457     if ( MP2::OBJ_MONSTER == world.GetTiles( center ).GetObject() )
458         result.push_back( center );
459     if ( x < width - 1 )
460         validateAndInsert( center + 1 );
461 
462     if ( y < world.h() - 1 ) {
463         if ( x > 0 )
464             validateAndInsert( center + width - 1 );
465 
466         validateAndInsert( center + width );
467 
468         if ( x < width - 1 )
469             validateAndInsert( center + width + 1 );
470     }
471 
472     return result;
473 }
474 
GetApproximateDistance(const int32_t pos1,const int32_t pos2)475 uint32_t Maps::GetApproximateDistance( const int32_t pos1, const int32_t pos2 )
476 {
477     const fheroes2::Point point1( GetPoint( pos1 ) );
478     const fheroes2::Point point2( GetPoint( pos2 ) );
479 
480     const fheroes2::Size sz( std::abs( point1.x - point2.x ), std::abs( point1.y - point2.y ) );
481     // diagonal move costs 1.5 as much
482     return std::max( sz.width, sz.height ) + std::min( sz.width, sz.height ) / 2;
483 }
484 
ReplaceRandomCastleObjectId(const fheroes2::Point & center)485 void Maps::ReplaceRandomCastleObjectId( const fheroes2::Point & center )
486 {
487     // Reset castle ID
488     for ( int32_t y = -3; y < 2; ++y ) {
489         for ( int32_t x = -2; x < 3; ++x ) {
490             Maps::Tiles & tile = world.GetTiles( center.x + x, center.y + y );
491 
492             if ( MP2::OBJN_RNDCASTLE == tile.GetObject() || MP2::OBJN_RNDTOWN == tile.GetObject() ) {
493                 tile.SetObject( MP2::OBJN_CASTLE );
494             }
495         }
496     }
497 
498     // restore center ID
499     world.GetTiles( center.x, center.y ).SetObject( MP2::OBJ_CASTLE );
500 }
501 
UpdateCastleSprite(const fheroes2::Point & center,int race,bool isCastle,bool isRandom)502 void Maps::UpdateCastleSprite( const fheroes2::Point & center, int race, bool isCastle, bool isRandom )
503 {
504     /*
505     Castle/Town object image consists of 42 tile sprites:
506     10 base tiles (OBJNTWBA) with 16 shadow tiles on left side (OBJNTWSH) overlayed by 16 town tiles (OBJNTOWN)
507 
508     Shadows (OBJNTWSH)  Castle (OBJNTOWN)
509                               0
510        32 33 34 35      1  2  3  4  5
511     36 37 38 39 40      6  7  8  9 10
512        41 42 43 44     11 12 13 14 15
513           45 46 47
514     */
515 
516     // correct only RND town and castle
517     const Maps::Tiles & entranceTile = world.GetTiles( center.x, center.y );
518     const MP2::MapObjectType objectType = entranceTile.GetObject();
519     const uint32_t castleID = entranceTile.GetObjectUID();
520 
521     if ( isRandom && ( objectType != MP2::OBJ_RNDCASTLE && objectType != MP2::OBJ_RNDTOWN ) ) {
522         DEBUG_LOG( DBG_GAME, DBG_WARN,
523                    "incorrect object"
524                        << ", index: " << GetIndexFromAbsPoint( center.x, center.y ) );
525         return;
526     }
527 
528     int raceIndex = 0; // Race::KNIGHT
529     switch ( race ) {
530     case Race::BARB:
531         raceIndex = 1;
532         break;
533     case Race::SORC:
534         raceIndex = 2;
535         break;
536     case Race::WRLK:
537         raceIndex = 3;
538         break;
539     case Race::WZRD:
540         raceIndex = 4;
541         break;
542     case Race::NECR:
543         raceIndex = 5;
544         break;
545     default:
546         break;
547     }
548 
549     const int castleCoordinates[16][2] = { { 0, -3 }, { -2, -2 }, { -1, -2 }, { 0, -2 }, { 1, -2 }, { 2, -2 }, { -2, -1 }, { -1, -1 },
550                                            { 0, -1 }, { 1, -1 },  { 2, -1 },  { -2, 0 }, { -1, 0 }, { 0, 0 },  { 1, 0 },   { 2, 0 } };
551     const int shadowCoordinates[16][2] = { { -4, -2 }, { -3, -2 }, { -2, -2 }, { -1, -2 }, { -5, -1 }, { -4, -1 }, { -3, -1 }, { -2, -1 },
552                                            { -1, -1 }, { -4, 0 },  { -3, 0 },  { -2, 0 },  { -1, 0 },  { -3, 1 },  { -2, 1 },  { -1, 1 } };
553 
554     for ( int index = 0; index < 16; ++index ) {
555         const int fullTownIndex = index + ( isCastle ? 0 : 16 ) + raceIndex * 32;
556         const int lookupID = isRandom ? index + ( isCastle ? 0 : 16 ) : fullTownIndex;
557 
558         const int castleTile = GetIndexFromAbsPoint( center.x + castleCoordinates[index][0], center.y + castleCoordinates[index][1] );
559         if ( isValidAbsIndex( castleTile ) ) {
560             Tiles & tile = world.GetTiles( castleTile );
561 
562             if ( isRandom )
563                 tile.ReplaceObjectSprite( castleID, 38, 35 * 4, lookupID, fullTownIndex ); // OBJNTWRD to OBJNTOWN
564             else
565                 tile.UpdateObjectSprite( castleID, 35, 35 * 4, -16 ); // no change in tileset
566 
567             if ( index == 0 ) {
568                 TilesAddon * addon = tile.FindAddonLevel2( castleID );
569                 if ( addon && MP2::GetICNObject( addon->object ) == ICN::OBJNTWRD ) {
570                     addon->object -= 12;
571                     addon->index = fullTownIndex - 16;
572                 }
573             }
574         }
575 
576         const int shadowTile = GetIndexFromAbsPoint( center.x + shadowCoordinates[index][0], center.y + shadowCoordinates[index][1] );
577         if ( isValidAbsIndex( shadowTile ) ) {
578             if ( isRandom )
579                 world.GetTiles( shadowTile ).ReplaceObjectSprite( castleID, 38, 37 * 4, lookupID + 32, fullTownIndex ); // OBJNTWRD to OBJNTWSH
580             else
581                 world.GetTiles( shadowTile ).UpdateObjectSprite( castleID, 37, 37 * 4, -16 ); // no change in tileset
582         }
583     }
584 }
585 
operator >>(StreamBase & sb,IndexObject & st)586 StreamBase & operator>>( StreamBase & sb, IndexObject & st )
587 {
588     return sb >> st.first >> st.second;
589 }
590 
operator >>(StreamBase & sb,ObjectColor & st)591 StreamBase & operator>>( StreamBase & sb, ObjectColor & st )
592 {
593     return sb >> st.first >> st.second;
594 }
595