1002 lines
32 KiB
C++
1002 lines
32 KiB
C++
#include "stdafx.h"
|
|
|
|
#include "GameWarDistributionChart.h"
|
|
#include "GameInstanceDungeon.h"
|
|
#include "Monster.h"
|
|
#include "StatusManager.h"
|
|
#include "TriggerManagerEx.h"
|
|
#include "SummonManager.h"
|
|
|
|
#include "MovingPlanner.h"
|
|
#include <World/SSQ/SSQHandlerInGame.h>
|
|
|
|
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// <SSQDistributionChart>
|
|
//
|
|
ClosedSpaceRangeTiles*
|
|
SSQDistributionChart::ms_pClosedSpaceRangeTiles = 0;
|
|
|
|
|
|
SSQDistributionChart::SSQDistributionChart()
|
|
: m_pSSQField(0)
|
|
, m_pGameField(0)
|
|
, ClosedSpaceViewBox( m_ViewBoxForClosedSpace )
|
|
, CenterPosition( m_CenterPosition )
|
|
, ClosedSpaceRangeQ2( SSQClosedSpace_DamageRadius*SSQClosedSpace_DamageRadius ) // 반경 24.5m
|
|
{
|
|
SetVector( &m_CenterPosition, SSQClosedSpaceCenter_X, SSQClosedSpaceCenter_Y, SSQClosedSpaceCenter_Z );
|
|
#ifdef _NA_004830_20120511_1202_SSQ_CLOSESPACE_DAMAGE_RANGE
|
|
const FLOAT closedSpaceDamageRange = 24.5f; // 검색 반경 24.5m
|
|
#else
|
|
const FLOAT closedSpaceDamageRange = 22.0f; // 검색 반경 22m
|
|
#endif // _NA_004830_20120511_1202_SSQ_CLOSESPACE_DAMAGE_RANGE
|
|
|
|
|
|
WzVector radius3D = { closedSpaceDamageRange, closedSpaceDamageRange, closedSpaceDamageRange };
|
|
m_ViewBoxForClosedSpace.SetInit3D( &m_CenterPosition, &radius3D );
|
|
}
|
|
|
|
SSQDistributionChart::~SSQDistributionChart()
|
|
{
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
GameField*
|
|
SSQDistributionChart::SetAndGetSSQField( GameInstanceDungeon* const pSSQField )
|
|
{
|
|
GameField* const pField = pSSQField->GetEntryField();
|
|
|
|
m_pSSQField = pSSQField;
|
|
m_pGameField = pField;
|
|
m_TotalTileNumber = pField->GetMovingPlanner()->GetNumPathTile();
|
|
|
|
return pField;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
class ClosedSpaceRangeTiles
|
|
{
|
|
typedef SSQDistributionChart SSQDC;
|
|
public:
|
|
struct ArrayInfo
|
|
{
|
|
DWORD Size;
|
|
WORD* pArray;
|
|
};
|
|
|
|
const ArrayInfo& ClosedSpaceTiles;
|
|
|
|
inline INT GetUnusefulTiles() { return m_UnusefulTiles; }
|
|
|
|
private: ArrayInfo m_csTiles;
|
|
INT m_UnusefulTiles;
|
|
|
|
public: ClosedSpaceRangeTiles(
|
|
const INT max_tiles
|
|
, GameField* pGameField
|
|
, const SSQDC::ViewBoxForClosedSpace& ClosedSpaceViewBox
|
|
, const WzVector& CenterPosition
|
|
, const FLOAT ClosedSpaceRangeQ2
|
|
)
|
|
: ClosedSpaceTiles( m_csTiles )
|
|
{
|
|
m_csTiles.Size = (DWORD)max_tiles;
|
|
const SIZE_T sizeof_TileWeight = sizeof(WORD)*m_csTiles.Size;
|
|
m_csTiles.pArray = (WORD*)TAllocMalloc( sizeof_TileWeight );
|
|
ZeroMemory( m_csTiles.pArray, sizeof_TileWeight );
|
|
|
|
using namespace nsAI;
|
|
MovingPlanner* const pMovingPlanner = pGameField->GetMovingPlanner();
|
|
|
|
for( DWORD loop=2 ; loop ; --loop )
|
|
{
|
|
for( INT iTile=0 ; iTile<max_tiles ; ++iTile )
|
|
{
|
|
if( SSQDC::WEIGHT_CLOSEDSPACE == m_csTiles.pArray[iTile] )
|
|
continue;
|
|
WzVector pos = pMovingPlanner->GetRandomPosInTile( iTile );
|
|
|
|
const BOOLEAN isFind1 = !(VectorLength2( &(CenterPosition-pos) ) > ClosedSpaceRangeQ2);
|
|
if( isFind1 )
|
|
m_csTiles.pArray[iTile] = SSQDC::WEIGHT_CLOSEDSPACE;
|
|
}
|
|
}
|
|
}
|
|
|
|
~ClosedSpaceRangeTiles()
|
|
{
|
|
if( m_csTiles.pArray )
|
|
TAllocFree( m_csTiles.pArray );
|
|
m_csTiles.Size = 0;
|
|
m_csTiles.pArray = 0;
|
|
}
|
|
|
|
//private: VOID _Calulation(
|
|
// GameField* pGameField
|
|
// , const SSQDC::ViewBoxForClosedSpace& rView
|
|
// , const WzVector& CenterPosition
|
|
// , const FLOAT ClosedSpaceRangeQ2
|
|
// )
|
|
};
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
INT
|
|
SSQDistributionChart::RandomDistribution( const SSQInfo* const pSSQInfo, const MatchCheckCodeFilters* const pCheckFilter )
|
|
{
|
|
if( !(m_pSSQField && m_SectorCount) )
|
|
return 0;
|
|
|
|
nsAI::MovingPlanner* const pMovingPlanner = m_pGameField->GetMovingPlanner();
|
|
|
|
ClosedSpaceRangeTiles::ArrayInfo weight_for_sector;
|
|
{
|
|
weight_for_sector.Size = m_SectorCount;
|
|
const SIZE_T sizeof_SectorWeight= sizeof(WORD)*weight_for_sector.Size;
|
|
weight_for_sector.pArray = (WORD*)TAllocMalloc( sizeof_SectorWeight );
|
|
ZeroMemory( weight_for_sector.pArray, sizeof_SectorWeight );
|
|
}
|
|
|
|
ClosedSpaceRangeTiles::ArrayInfo weight_for_tile;
|
|
{
|
|
weight_for_tile.Size = (DWORD)m_TotalTileNumber;
|
|
const SIZE_T sizeof_TileWeight = sizeof(WORD)*weight_for_tile.Size;
|
|
weight_for_tile.pArray = (WORD*)TAllocMalloc( sizeof_TileWeight );
|
|
ZeroMemory( weight_for_tile.pArray, sizeof_TileWeight );
|
|
}
|
|
|
|
typedef GameField::_CHARACTER_HASH _CHARACTER_HASH;
|
|
const _CHARACTER_HASH& rCharacters = m_pGameField->GetRefCharacterList();
|
|
const DWORD character_num = (DWORD)rCharacters.size(); // ASSERT( character_num );
|
|
DWORD _player_num = (DWORD)m_pGameField->GetRefPlayerList().size();
|
|
ASSERT( _player_num < SSQ_MAX_CHARACTERS );
|
|
const DWORD player_num = min( _player_num, SSQ_MAX_CHARACTERS );
|
|
|
|
DWORD weight_sum_of_tile = 0;
|
|
|
|
// (CHANGES) (100107) (WAVERIX) add player random warp
|
|
// (NOTE) 하기 WEIGHT_PLAYER(5)->2는 최소한 워프가 랜덤하게 이뤄지는 것처럼 유도하기 위함.
|
|
FOREACH_CONTAINER( const _CHARACTER_HASH::value_type& rCharacter, rCharacters, _CHARACTER_HASH )
|
|
{
|
|
Character* const pCharacter = rCharacter.second;
|
|
const INT iTileOfChar = pCharacter->GetPathExplorer()->GetTile();
|
|
if( !( (DWORD)iTileOfChar < weight_for_tile.Size ) )
|
|
continue;
|
|
const DWORD sector_index = GetSectorIndex( pCharacter->GetPosPtr() );
|
|
|
|
const WORD weight = pCharacter->IsEqualObjectKind(PLAYER_OBJECT) ? 2 //WEIGHT_PLAYER
|
|
: WEIGHT_MONSTER;
|
|
|
|
++weight_sum_of_tile;
|
|
weight_for_tile.pArray[iTileOfChar] += weight;
|
|
weight_for_sector.pArray[sector_index] += weight;
|
|
}
|
|
|
|
const SSQInfo::SUMMONDED_NPC_INFO& rNpcInfo = pSSQInfo->m_SummonedNpcInfo;
|
|
const DWORD npc_list_count = rNpcInfo.Count;
|
|
|
|
{ // (For Test) 애초 유저가 없는 상황에서 이 루틴이 실행되면 안될 것이다
|
|
weight_sum_of_tile = weight_sum_of_tile ? weight_sum_of_tile : 1;
|
|
};
|
|
|
|
// Numeric Formula
|
|
#define WEITGHT_AVG_CAL( numerator, denominator ) \
|
|
(DWORD)( ( ( (2*(numerator))+(denominator) )/(denominator) ) + 1 )
|
|
|
|
const DWORD weight_sum_of_sector = (DWORD)(weight_sum_of_tile/60);
|
|
DWORD available_tile_num = weight_for_tile.Size;
|
|
DWORD weight_avg_of_tile = WEITGHT_AVG_CAL(weight_sum_of_tile, available_tile_num );
|
|
|
|
|
|
static ClosedSpaceRangeTiles CSRT(
|
|
weight_for_tile.Size
|
|
, m_pGameField
|
|
, ClosedSpaceViewBox
|
|
, CenterPosition
|
|
, ClosedSpaceRangeQ2
|
|
);
|
|
|
|
ms_pClosedSpaceRangeTiles = &CSRT;
|
|
available_tile_num -= (DWORD)CSRT.GetUnusefulTiles();
|
|
CopyMemory( weight_for_tile.pArray, CSRT.ClosedSpaceTiles.pArray, sizeof(WORD)*weight_for_tile.Size );
|
|
|
|
RandomNumberGenerator::AddRandomNumberEntropy( character_num );
|
|
|
|
_CHARACTER_HASH::const_iterator character_it = rCharacters.begin();
|
|
_CHARACTER_HASH::const_iterator character_itend = rCharacters.end();
|
|
|
|
const INT loop_limit = SSQ_MAX_CHARACTERS - player_num;
|
|
DWORD loop = 300;
|
|
INT mob_num = 0;
|
|
BOOLEAN skip_player_warp = false;
|
|
|
|
for( ; loop && mob_num<loop_limit ; --loop )
|
|
{
|
|
DWORD tile_no = -1;
|
|
WORD* pTileWeight = 0;
|
|
WORD* pSectorWeight = 0;
|
|
WzVector regen_pos;
|
|
|
|
for( BOOLEAN bAccept = FALSE ; !bAccept ; )
|
|
{
|
|
tile_no = RandomNumberGenerator::GetRandomNumberRange( 0, weight_for_tile.Size );
|
|
pTileWeight = &weight_for_tile.pArray[tile_no];
|
|
|
|
if( (DWORD)*pTileWeight > weight_avg_of_tile )
|
|
continue;
|
|
|
|
regen_pos = pMovingPlanner->GetRandomPosInTile( tile_no );
|
|
const DWORD sector_index = GetSectorIndex( ®en_pos );
|
|
pSectorWeight = &weight_for_sector.pArray[sector_index];
|
|
|
|
if( (DWORD)*pSectorWeight > weight_sum_of_sector )
|
|
continue;
|
|
|
|
bAccept = TRUE;
|
|
}
|
|
|
|
const INT tile_attr = pMovingPlanner->GetAttribute( tile_no );
|
|
if( DISABLE_TILE_ATTR & tile_attr )
|
|
{
|
|
*pTileWeight = WEIGHT_DISABLE;
|
|
*pSectorWeight = WEIGHT_DISABLE;
|
|
continue;
|
|
}
|
|
|
|
// caching된 정보로 이미 해결되었다.
|
|
////if( VectorLength2( &(CenterPosition-regen_pos) ) < ClosedSpaceRangeQ2 )
|
|
//if( ClosedSpaceViewBox.IsValidCoordinateXYZ( ®en_pos ) )
|
|
//{
|
|
// *pTileWeight = WEIGHT_CLOSEDSPACE;
|
|
// *pSectorWeight = WEIGHT_CLOSEDSPACE;
|
|
// --available_tile_num;
|
|
// continue;
|
|
//}
|
|
|
|
// (CHANGES) (100107) (WAVERIX) add player random warp
|
|
if (skip_player_warp == false && (skip_player_warp = character_it != character_itend))
|
|
{
|
|
Character* character = character_it->second;
|
|
BOOLEAN is_player = !!character->IsEqualObjectKind(PLAYER_OBJECT);
|
|
m_pGameField->TeleportObject(character, ®en_pos, true);
|
|
++character_it;
|
|
*pTileWeight += is_player ? WEIGHT_PLAYER : WEIGHT_MONSTER;
|
|
*pSectorWeight += is_player ? WEIGHT_PLAYER : WEIGHT_MONSTER;
|
|
continue;
|
|
}
|
|
|
|
*pTileWeight += WEIGHT_MONSTER;
|
|
*pSectorWeight += WEIGHT_MONSTER;
|
|
|
|
++weight_sum_of_tile;
|
|
weight_avg_of_tile = WEITGHT_AVG_CAL(weight_sum_of_tile, available_tile_num);
|
|
|
|
const MONSTERCODE selected_npc = pSSQInfo->GetRandomRegenMonster();
|
|
//const DWORD selected_npc = RandomNumberGenerator::GetRandomNumberRange( 0, npc_list_count );
|
|
//// <- 잘 할 것... (확률로 계산된다.
|
|
// TODO(WAVERIX)
|
|
//const MONSTERCODE npcCode = rNpcInfo.List[selected_npc].m_NPCCode;
|
|
Monster* const pMonster = m_pGameField->CreateMonster( selected_npc, ®en_pos );
|
|
// <- SSQMonster 일 것이다.
|
|
if( !pMonster )
|
|
{
|
|
SUNLOG( eCRITICAL_LOG
|
|
, "Can't Create Monster { MC:%u FC:%u MOB:%u } X:Y:Z = { %.2f : %.2f : %.2f }"
|
|
, m_pSSQField->GetMapCode(), m_pGameField->GetFieldCode(), selected_npc
|
|
, regen_pos.x, regen_pos.y, regen_pos.z );
|
|
continue; // 무한 루프 가능성은 염두해 두자.
|
|
}
|
|
|
|
StatusManager* const pStatusManager = pMonster->GetStatusManager();
|
|
pStatusManager->Status.On_FilterControl( TRUE, TRUE, pCheckFilter );
|
|
pStatusManager->ChangeInterState( eCHAR_STATE_SSQ_CTRL_BLOCK_ATTACK, TRUE );
|
|
// last
|
|
++mob_num;
|
|
}
|
|
|
|
{ // apply closed space tile weight
|
|
const WORD* pIt = weight_for_tile.pArray;
|
|
const WORD* const pItEnd = weight_for_tile.pArray + weight_for_tile.Size;
|
|
|
|
typedef nsAI::PlanCmd PlanCmd;
|
|
for( INT iTileNo=0 ; pIt!=pItEnd ; ++pIt, ++iTileNo )
|
|
{
|
|
if( WEIGHT_CLOSEDSPACE != *pIt )
|
|
continue;
|
|
|
|
const PlanCmd::TileWeightArg args(
|
|
PlanCmd::eTileWeight_SetWeight
|
|
, iTileNo
|
|
, nsAI::eWeight_ClosedSpace
|
|
);
|
|
pMovingPlanner->PathPlan_Command( args );
|
|
}
|
|
};
|
|
|
|
TAllocFree( weight_for_sector.pArray );
|
|
TAllocFree( weight_for_tile.pArray );
|
|
|
|
return mob_num;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
BOOLEAN
|
|
SSQDistributionChart::Earthquake_Indication(
|
|
const SSQPattern* const pSSQPattern, const BYTE step_array_index
|
|
)
|
|
{
|
|
if( !(pSSQPattern->m_BanLiftStep > step_array_index) )
|
|
return FALSE;
|
|
|
|
typedef nsAI::PlanCmd PlanCmd;
|
|
nsAI::MovingPlanner* const pMovingPlanner = m_pGameField->GetMovingPlanner();
|
|
|
|
typedef SSQInfoParser::SSQTILEINFO_TABLE SSQTILEINFO_TABLE;
|
|
const SSQTILEINFO_TABLE& rPatternInfos = SSQInfoParser::Instance()->GetRefSSQPatternInfo();
|
|
|
|
const SSQPattern::sSINGLE_STEP& rTileInfo = pSSQPattern->m_StepGroup[step_array_index];
|
|
|
|
const SSQPattern::OBJECT_ID_T* pIt = &rTileInfo.m_List[0];
|
|
const SSQPattern::OBJECT_ID_T* const pEnd = &rTileInfo.m_List[rTileInfo.m_ListSize];
|
|
for( ; pIt!=pEnd ; ++pIt )
|
|
{
|
|
const SSQPattern::OBJECT_ID_T obj_id = *pIt;
|
|
SSQTILEINFO_TABLE::const_iterator itFind = rPatternInfos.find( obj_id );
|
|
if( itFind == rPatternInfos.end() )
|
|
continue;
|
|
|
|
const _AREA_TILE_INFO& rTiles = itFind->second;
|
|
|
|
const INT* pIt = &rTiles.piTileIndex[0];
|
|
const INT* const pEnd = &rTiles.piTileIndex[rTiles.wNumberOfTiles];
|
|
for( ; pIt!=pEnd ; ++pIt )
|
|
{
|
|
const INT tileID = *pIt;
|
|
ASSERT( m_TotalTileNumber > (DWORD)tileID );
|
|
|
|
const PlanCmd::TileWeightArg args(
|
|
PlanCmd::eTileWeight_SetWeight
|
|
, tileID
|
|
, nsAI::eWeight_Earthquake_Indi
|
|
);
|
|
pMovingPlanner->PathPlan_Command( args );
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
BOOLEAN
|
|
SSQDistributionChart::Earthquake(
|
|
const SSQPattern* const pSSQPattern, const BYTE step_array_index
|
|
, nsSSQ::MSGSUB_SSQ_PROTOCOL_CONTROL_FORCED_POSITION_CORRECTION& INOUT rMsgFPC
|
|
)
|
|
{
|
|
if( !(pSSQPattern->m_BanLiftStep > step_array_index) )
|
|
return FALSE;
|
|
|
|
nsAI::MovingPlanner* const pMovingPlanner = m_pGameField->GetMovingPlanner();
|
|
|
|
typedef SSQInfoParser::SSQTILEINFO_TABLE SSQTILEINFO_TABLE;
|
|
const SSQTILEINFO_TABLE& rPatternInfos = SSQInfoParser::Instance()->GetRefSSQPatternInfo();
|
|
|
|
const SSQPattern::sSINGLE_STEP& rTileInfo = pSSQPattern->m_StepGroup[step_array_index];
|
|
|
|
const SSQPattern::OBJECT_ID_T* pIt = &rTileInfo.m_List[0];
|
|
const SSQPattern::OBJECT_ID_T* const pEnd = &rTileInfo.m_List[rTileInfo.m_ListSize];
|
|
for( ; pIt!=pEnd ; ++pIt )
|
|
{
|
|
const SSQPattern::OBJECT_ID_T obj_id = *pIt;
|
|
SSQTILEINFO_TABLE::const_iterator itFind = rPatternInfos.find( obj_id );
|
|
if( itFind == rPatternInfos.end() )
|
|
continue;
|
|
|
|
const _AREA_TILE_INFO& rTiles = itFind->second;
|
|
|
|
const INT* pIt = &rTiles.piTileIndex[0];
|
|
const INT* const pEnd = &rTiles.piTileIndex[rTiles.wNumberOfTiles];
|
|
for( ; pIt!=pEnd ; ++pIt )
|
|
{
|
|
const INT tileID = *pIt;
|
|
ASSERT( m_TotalTileNumber > (DWORD)tileID );
|
|
pMovingPlanner->SetAttribute( tileID, PTA_PLAYER_FALLDOWN_TILE );
|
|
}
|
|
}
|
|
|
|
{
|
|
rMsgFPC.Count = 0;
|
|
|
|
typedef nsSSQ::MSGSUB_SSQ_PROTOCOL_CONTROL_FORCED_POSITION_CORRECTION::NODE FPC_NODE;
|
|
FPC_NODE* pIt = &rMsgFPC.PositionInfo[0];
|
|
FPC_NODE* const pEnd= &rMsgFPC.PositionInfo[_countof(rMsgFPC.PositionInfo)];
|
|
|
|
// Character Control
|
|
typedef GameField::_CHARACTER_HASH _CHARACTER_HASH;
|
|
const _CHARACTER_HASH& rCharacters = m_pGameField->GetRefCharacterList();
|
|
FOREACH_CONTAINER( const _CHARACTER_HASH::value_type& rCharacter, rCharacters, _CHARACTER_HASH )
|
|
{
|
|
//#ifdef _NA_004126_20120111_CHANGE_GAME_SERVER_OBJECT_FACTORY
|
|
Object* const object = rCharacter.second;
|
|
if (object != NULL && object->IsDeleted())
|
|
{
|
|
const Object::Key key = object->GetObjectKey();
|
|
SUNLOG(
|
|
eCRITICAL_LOG,
|
|
_T("|%s|%d|Object is already deleted.|ObjectKey = %d|"),
|
|
__FUNCTION__,
|
|
__LINE__,
|
|
key
|
|
);
|
|
continue;
|
|
}
|
|
//#endif
|
|
Character* const pCharacter = rCharacter.second;
|
|
|
|
const DWORD curHP = pCharacter->GetHP();
|
|
StatusField& rStatus = pCharacter->GetStatusManager()->Status;
|
|
const BOOLEAN bApplyDead =
|
|
curHP && !( (rStatus.FIELD&rStatus.ENABLE_GM_UNDEAD) || (rStatus.FIELD&rStatus.ENABLE_OBSERVER_MODE) );
|
|
|
|
const INT iTileOfChar = pCharacter->GetPathExplorer()->GetTile();
|
|
if( !( (DWORD)iTileOfChar < m_TotalTileNumber ) )
|
|
{ // abnormal status , dead process
|
|
if( bApplyDead )
|
|
{
|
|
pCharacter->ClearKillerInfo();
|
|
pCharacter->SetHP(0);
|
|
pCharacter->OnDead();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if( pIt != pEnd )
|
|
{
|
|
pIt->ObjectKey = pCharacter->GetObjectKey();
|
|
pIt->Position = *pCharacter->GetPosPtr();
|
|
++pIt;
|
|
++rMsgFPC.Count;
|
|
}
|
|
|
|
const WORD tile_attr = pMovingPlanner->GetAttribute( iTileOfChar );
|
|
if( tile_attr & PTA_PLAYER_FALLDOWN_TILE )
|
|
{ // dead process
|
|
if( bApplyDead )
|
|
{
|
|
pCharacter->ClearKillerInfo();
|
|
pCharacter->SetHP(0);
|
|
pCharacter->OnDead();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
BOOLEAN
|
|
SSQDistributionChart::Thunderbolt(
|
|
const SSQInfo* const pSSQInfo
|
|
, const BOOLEAN bShowmanship
|
|
, const WzVector* const pRange
|
|
, const BOOLEAN bProcOutRangeClosedSpace, const DWORD max_value
|
|
, nsSSQ::MSGSUB_SSQ_PROTOCOL_TRAP_THUNDERBOLT_EXECUTE& INOUT rMsgTTE
|
|
, nsSSQ::MSGSUB_SSQ_PROTOCOL_CONTROL_DAMAGE_PROCESS& INOUT rMsgCDP
|
|
)
|
|
{
|
|
const DWORD rangeX = m_Boundaries[SG_X_MAX];
|
|
const DWORD rangeY = m_Boundaries[SG_Y_MAX];
|
|
|
|
const DWORD upperbound = (min( rMsgTTE.MAX_SIZE, max_value )) + 1; // ubound-1까지 random value이므로...
|
|
const DWORD minVal = (min( rMsgTTE.MAX_SIZE, max_value )) >>1;
|
|
ASSERT( (minVal+1) < upperbound );
|
|
|
|
typedef RandomNumberGenerator RNG;
|
|
const DWORD loop = RNG::GetRandomNumberRange( minVal, upperbound );
|
|
rMsgTTE.Count = (SSQInfo::COUNT_T)loop;
|
|
WzVector* pPosVector = &rMsgTTE.Position[0];
|
|
|
|
const DWORD minCoord = bProcOutRangeClosedSpace ? 12500 : 17800 ;
|
|
const DWORD maxCoord = bProcOutRangeClosedSpace ? 27000 : 22200 ;
|
|
|
|
for( DWORD idx=0 ; idx<loop ; )
|
|
{
|
|
// [127, 270] : x==y
|
|
const FLOAT coordX = (FLOAT)RNG::GetRandomNumberRange( minCoord, maxCoord /*rangeX*/ )/100.f;
|
|
const FLOAT coordY = (FLOAT)RNG::GetRandomNumberRange( minCoord, maxCoord /*rangeY*/ )/100.f;
|
|
|
|
SetVector( pPosVector, coordX, coordY, 9.0f );
|
|
|
|
const BOOLEAN inRangeClosedSpace = ClosedSpaceViewBox.IsValidCoordinateXYZ( pPosVector );
|
|
const BOOLEAN need_skip = bProcOutRangeClosedSpace ? inRangeClosedSpace : !inRangeClosedSpace ;
|
|
|
|
if( need_skip )
|
|
continue;
|
|
|
|
++idx;
|
|
++pPosVector;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
rMsgCDP.Count = 0;
|
|
|
|
if( bShowmanship )
|
|
return TRUE;
|
|
|
|
pPosVector = &rMsgTTE.Position[0];
|
|
|
|
ViewBoxCubic viewbox[rMsgTTE.MAX_SIZE];
|
|
ViewBoxCubic* pAlignedViewBox[rMsgTTE.MAX_SIZE] = { 0, };
|
|
for( DWORD idx=0 ; idx<loop ; ++idx, ++pPosVector )
|
|
{
|
|
ViewBoxCubic* const pView = &viewbox[idx];
|
|
pAlignedViewBox[idx] = pView;
|
|
pView->SetInit2D( pPosVector, pRange );
|
|
}
|
|
|
|
const DAMAGETYPE damageNPC = pSSQInfo->m_ThunderboltInfo.m_NpcDamage;
|
|
const DAMAGETYPE damagePC = pSSQInfo->m_ThunderboltInfo.m_PlayerDamage;
|
|
|
|
typedef GameField::_CHARACTER_HASH _CHARACTER_HASH;
|
|
const _CHARACTER_HASH& rCharacters = m_pGameField->GetRefCharacterList();
|
|
FOREACH_CONTAINER( const _CHARACTER_HASH::value_type& rCharacter, rCharacters, _CHARACTER_HASH )
|
|
{
|
|
//#ifdef _NA_004126_20120111_CHANGE_GAME_SERVER_OBJECT_FACTORY
|
|
Object* const object = rCharacter.second;
|
|
if (object != NULL && object->IsDeleted())
|
|
{
|
|
const Object::Key key = object->GetObjectKey();
|
|
SUNLOG(
|
|
eCRITICAL_LOG,
|
|
_T("|%s|%d|Object is already deleted.|ObjectKey = %d|"),
|
|
__FUNCTION__,
|
|
__LINE__,
|
|
key
|
|
);
|
|
continue;
|
|
}
|
|
//#endif
|
|
Character* const pCharacter = rCharacter.second;
|
|
|
|
const WzVector* const pPosition = pCharacter->GetPosPtr();
|
|
|
|
// Damage Range Processing
|
|
ViewBoxCubic** ppView = &pAlignedViewBox[0];
|
|
for( DWORD count=loop ; count ; --count, ++ppView )
|
|
{
|
|
ViewBoxCubic* const pView = *ppView;
|
|
if( !( pView && pView->IsValidCoordinateXY( pPosition ) ) )
|
|
continue;
|
|
|
|
// by script data
|
|
const sPOLICY_CHARACTER& Policyment = pCharacter->Policyment;
|
|
const BOOLEAN isPlayer = !!(Policyment.FIELD & Policyment.PCC_IN_SSQ_FIELD_PLAYER);
|
|
const DAMAGETYPE damage = isPlayer ? damagePC : damageNPC;
|
|
|
|
const DWORD curHP = pCharacter->GetHP();
|
|
const DWORD nextHP
|
|
= ( (DWORD)damage < curHP )
|
|
? curHP - damage
|
|
: 0
|
|
;
|
|
|
|
StatusField& rStatus = pCharacter->GetStatusManager()->Status;
|
|
const BOOLEAN isObserver = !!(rStatus.FIELD&rStatus.ENABLE_OBSERVER_MODE);
|
|
const BOOLEAN bApplyDead =
|
|
curHP && !( (rStatus.FIELD&rStatus.ENABLE_GM_UNDEAD) || isObserver );
|
|
|
|
if( nextHP )
|
|
{
|
|
pCharacter->SetHP( nextHP );
|
|
}
|
|
else
|
|
{
|
|
if( bApplyDead )
|
|
{
|
|
pCharacter->ClearKillerInfo();
|
|
pCharacter->SetHP(0);
|
|
pCharacter->OnDead();
|
|
}
|
|
}
|
|
|
|
if( !isObserver && (rMsgCDP.Count < rMsgCDP.MAX_SIZE) )
|
|
{
|
|
DAMAGE_INFO& rDamage = rMsgCDP.Damages[rMsgCDP.Count];
|
|
rDamage.m_dwTargetKey = pCharacter->GetObjectKey();
|
|
rDamage.m_wDamage = damage;
|
|
rDamage.m_dwTargetHP = pCharacter->GetHP();
|
|
++rMsgCDP.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
BOOLEAN
|
|
SSQDistributionChart::ClosedSpace_RemoveDanger(
|
|
const SSQInfo* const pSSQInfo
|
|
)
|
|
{
|
|
if( !ms_pClosedSpaceRangeTiles )
|
|
return FALSE;
|
|
|
|
nsAI::MovingPlanner* const pMovingPlanner = m_pGameField->GetMovingPlanner();
|
|
|
|
// release closed space tile weight
|
|
const ClosedSpaceRangeTiles::ArrayInfo& rArrayInfo = ms_pClosedSpaceRangeTiles->ClosedSpaceTiles;
|
|
const WORD* pIt = rArrayInfo.pArray;
|
|
const WORD* const pItEnd = rArrayInfo.pArray + rArrayInfo.Size;
|
|
|
|
typedef nsAI::PlanCmd PlanCmd;
|
|
for( INT iTileNo=0 ; pIt!=pItEnd ; ++pIt, ++iTileNo )
|
|
{
|
|
if( WEIGHT_CLOSEDSPACE != *pIt )
|
|
continue;
|
|
|
|
const PlanCmd::TileWeightArg args(
|
|
PlanCmd::eTileWeight_SetWeight
|
|
, iTileNo
|
|
, nsAI::eWeight_None
|
|
);
|
|
pMovingPlanner->PathPlan_Command( args );
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
BOOLEAN
|
|
SSQDistributionChart::ClosedSpace(
|
|
const SSQInfo* const pSSQInfo
|
|
, const BYTE earthquake_step
|
|
, nsSSQ::MSGSUB_SSQ_PROTOCOL_TRAP_CLOSEDSPACE_EXECUTE& INOUT rMsgTCE
|
|
)
|
|
{
|
|
rMsgTCE.Count = 0;
|
|
|
|
const BYTE earthquake_step_rev = min((SSQInfo::MI_EARTHQUAKE_SECTION_MAX-1), earthquake_step);
|
|
// damage = maxHP*(ratio/100)
|
|
const FLOAT damageNPC_ratio = pSSQInfo->m_ClosedSpaceInfo.m_NpcDamages[earthquake_step_rev]/100.0f;
|
|
const FLOAT damagePC_ratio = pSSQInfo->m_ClosedSpaceInfo.m_PlayerDamages[earthquake_step_rev]/100.0f;
|
|
//printf( "closed space spec : eq_step %u, ratio_base [%.2f:%.2f], calc_ratio [%.2f:%.2f]\n"
|
|
// , earthquake_step_rev
|
|
// , pSSQInfo->m_ClosedSpaceInfo.m_NpcDamages[earthquake_step_rev]
|
|
// , pSSQInfo->m_ClosedSpaceInfo.m_PlayerDamages[earthquake_step_rev]
|
|
// , damageNPC_ratio, damagePC_ratio
|
|
// );
|
|
//_NA000000_110210_ENHANCED_TARGET_SEARCH_ALGORITHM
|
|
// NOTE: f110308.1L, fake, sector 0 is exist
|
|
const SectorGridVR& rSectorGridVR = *m_pGameField->GetSectorGridVR(0, SSQClosedSpace_DamageRadius);
|
|
|
|
SectorGrid3x3 grid;
|
|
ViewBoxCubic viewboxVR;
|
|
rSectorGridVR.GetAroundSectors( &CenterPosition, &grid, &viewboxVR );
|
|
|
|
// 일괄 데미지 주는 거라 굳이 센터 먼저 처리할 필요 없다.
|
|
//GRID3x3_CENTER_PRIOR_SWAP( grid );
|
|
FOREACH_ARRAY( const SECTOR_IDX sector_idx, grid.Grid, SectorGrid3x3::GridArray )
|
|
{
|
|
if( INVALID_SECTOR_INDEX == sector_idx )
|
|
continue;
|
|
|
|
Sector* const pSector = m_pGameField->GetSector( sector_idx );
|
|
ns_object::SearchNodeConstIterator it = pSector->GetCharacterRoot();
|
|
const ns_object::SearchNodeConstIterator end = pSector->GetCharacterEnd();
|
|
|
|
while (const ns_object::SearchNode::LinkNode* link_node = it.ClimbToNext(end))
|
|
{
|
|
Character* const pCharacter = link_node->select.character_node.character;
|
|
if (obj_util::IsValid(pCharacter) == false)
|
|
{
|
|
continue;
|
|
}
|
|
const WzVector* const pPosition = pCharacter->GetPosPtr();
|
|
|
|
if( !m_ViewBoxForClosedSpace.IsValidCoordinateXY( pPosition )
|
|
#ifdef _NA_004830_20120511_1202_SSQ_CLOSESPACE_DAMAGE_RANGE
|
|
|| !m_ViewBoxForClosedSpace.IsValidCoordinateXY( pPosition, &CenterPosition )
|
|
#endif
|
|
)
|
|
continue;
|
|
|
|
// by script data
|
|
const sPOLICY_CHARACTER& Policyment = pCharacter->Policyment;
|
|
const BOOLEAN isPlayer = !!(Policyment.FIELD & Policyment.PCC_IN_SSQ_FIELD_PLAYER);
|
|
|
|
const FLOAT damageRatio = isPlayer ? damagePC_ratio : damageNPC_ratio;
|
|
const DWORD maxHP = pCharacter->GetMaxHP();
|
|
const DAMAGETYPE damage = DAMAGETYPE(maxHP*damageRatio);
|
|
|
|
const DWORD curHP = pCharacter->GetHP();
|
|
const DWORD nextHP
|
|
= ( (DWORD)damage < curHP )
|
|
? curHP - damage
|
|
: 0
|
|
;
|
|
|
|
StatusField& rStatus = pCharacter->GetStatusManager()->Status;
|
|
const BOOLEAN isObserver = !!(rStatus.FIELD&rStatus.ENABLE_OBSERVER_MODE);
|
|
const BOOLEAN bApplyDead =
|
|
curHP && !( (rStatus.FIELD&rStatus.ENABLE_GM_UNDEAD) || isObserver );
|
|
|
|
if( nextHP )
|
|
{
|
|
pCharacter->SetHP( nextHP );
|
|
}
|
|
else
|
|
{
|
|
if( bApplyDead )
|
|
{
|
|
pCharacter->ClearKillerInfo();
|
|
pCharacter->SetHP(0);
|
|
pCharacter->OnDead();
|
|
}
|
|
}
|
|
|
|
if( !isObserver && (rMsgTCE.Count < rMsgTCE.MAX_SIZE) )
|
|
{
|
|
DAMAGE_INFO& rDamage = rMsgTCE.Damages[rMsgTCE.Count];
|
|
rDamage.m_dwTargetKey = pCharacter->GetObjectKey();
|
|
rDamage.m_wDamage = damage;
|
|
rDamage.m_dwTargetHP = pCharacter->GetHP();
|
|
++rMsgTCE.Count;
|
|
}
|
|
} // sector loop
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
typedef GameWarSSQItemDistribution SIDN;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Ratio Table
|
|
const DWORD SSQItemRatioTableCols = 4;
|
|
const DWORD SSQItemRatioTableRows = 5;
|
|
typedef WORD SSQItemRatioRow[SSQItemRatioTableCols];
|
|
|
|
#define _SSQIRU(percent) ((percent)*100)
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// 임전하는 플레이어 수 : 확률 Col 선택기
|
|
static const WORD
|
|
cSSQItemRatioSelector[SSQItemRatioTableCols] =
|
|
{
|
|
10, 30, 50, 70
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// 임전 플레이어수(fix) + 지진 단계
|
|
static const SSQItemRatioRow
|
|
cSSQItemRatioTable[SSQItemRatioTableRows] =
|
|
{
|
|
{ _SSQIRU(10)+_SSQIRU( 5), _SSQIRU(10)+_SSQIRU(10), _SSQIRU(10)+_SSQIRU(15), _SSQIRU(10)+_SSQIRU(25) },//Begin FixRatio 10%
|
|
{ _SSQIRU(10)+_SSQIRU( 5), _SSQIRU(10)+_SSQIRU(10), _SSQIRU(10)+_SSQIRU(15), _SSQIRU(10)+_SSQIRU(25) },//Begin FixRatio 10%
|
|
{ _SSQIRU(20)+_SSQIRU( 5), _SSQIRU(20)+_SSQIRU(10), _SSQIRU(20)+_SSQIRU(15), _SSQIRU(20)+_SSQIRU(25) },//WarmUp FixRatio 20%
|
|
{ _SSQIRU(20)+_SSQIRU( 5), _SSQIRU(20)+_SSQIRU(10), _SSQIRU(20)+_SSQIRU(15), _SSQIRU(20)+_SSQIRU(25) },//WarmUp FixRatio 20%
|
|
{ _SSQIRU(35)+_SSQIRU( 5), _SSQIRU(35)+_SSQIRU(10), _SSQIRU(35)+_SSQIRU(15), _SSQIRU(35)+_SSQIRU(25) },//Climax FixRatio 35%
|
|
};
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
GameWarSSQItemDistribution::GameWarSSQItemDistribution()
|
|
: m_pSSQInfo(0)
|
|
, m_InitPlayers(0)
|
|
{
|
|
ZeroMemory( &m_DropInfoSpace, sizeof(m_DropInfoSpace) );
|
|
}
|
|
|
|
|
|
GameWarSSQItemDistribution::GameWarSSQItemDistribution( const SSQInfo* const pSSQInfo, const DWORD init_players )
|
|
: m_pSSQInfo( pSSQInfo )
|
|
, m_InitPlayers( init_players )
|
|
{
|
|
// 확률 선택기
|
|
{
|
|
DWORD idx = 0;
|
|
for( ; idx<SSQItemRatioTableCols ; ++idx )
|
|
if( m_InitPlayers <= cSSQItemRatioSelector[idx] )
|
|
break;
|
|
|
|
ASSERT( SSQItemRatioTableCols!=idx );
|
|
m_SelectedCol = ( SSQItemRatioTableCols!=idx ) ? (BYTE)idx : 0;
|
|
}
|
|
m_DropInfoSpace = m_pSSQInfo->m_RewardDropInfo;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
SSQInfo::REWARD_NODE
|
|
GameWarSSQItemDistribution::Drop( const BYTE earthquake_step )
|
|
{
|
|
const BYTE row_no = min( eRatio_MaxStep, earthquake_step );
|
|
//const eRatio row_no
|
|
// = value < 2 ? eRatio_Begin
|
|
// : value < 4 ? eRatio_WarmUp
|
|
// : eRatio_Climax
|
|
// ;
|
|
|
|
const DWORD var_ratio = cSSQItemRatioTable[row_no][m_SelectedCol];
|
|
const DWORD probability = RandomNumberGenerator::GetRandomNumberRange( 0, eRatio_MaxDegree );
|
|
|
|
SSQInfo::REWARD_NODE drops = { 0, 0 };
|
|
if( probability > var_ratio )
|
|
return drops;
|
|
|
|
const DWORD upperbound = m_DropInfoSpace.Count;
|
|
const DWORD selected_idx = RandomNumberGenerator::GetRandomNumberRange( 0, upperbound );
|
|
SSQInfo::REWARD_NODE& rNode = m_DropInfoSpace.List[selected_idx];
|
|
if( !rNode.m_RewardLimit )
|
|
return drops;
|
|
|
|
--rNode.m_RewardLimit;
|
|
drops.m_RewardCode = rNode.m_RewardCode;
|
|
drops.m_RewardLimit = 1;
|
|
|
|
return drops;
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
GameWarObserverMode::GameWarObserverMode( GameInstanceDungeon* const pIndun )
|
|
: m_pField( pIndun->GetEntryField() )
|
|
{
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
BOOLEAN
|
|
GameWarObserverMode::Enter( const nsSSQ::Rules::eC2G_ReqRule rules, Player* const pPlayer )
|
|
{
|
|
using namespace nsGrid;
|
|
using namespace nsSSQ;
|
|
typedef nsSSQ::Observer::Info Info;
|
|
if( !m_pField )
|
|
return FALSE;
|
|
|
|
const DWORD object_key = pPlayer->GetObjectKey();
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// #1 자신이 떠났다고 속인다. -.-
|
|
SECTOR_IDX coordinates = pPlayer->GetSectorIndex();
|
|
SectorGrid3x3 OUT grid;
|
|
|
|
const SectorGrid& rSectorGrid = m_pField->SectorGridRef;
|
|
if( !rSectorGrid.GetAroundSectors( coordinates, &grid ) )
|
|
return FALSE;
|
|
|
|
// Fake - Player::OnLeaveField
|
|
{
|
|
TriggerManagerEx* const pTriggerManager = m_pField->GetTriggerManager();
|
|
// (CHANGES) (f100614.4L) add a slot type for a trigger area linker to support a multi-area link
|
|
pPlayer->GetTriggerLinks()->Disconnect(pTriggerManager);
|
|
//pTriggerManager->Disconnected(pPlayer->GetTriggerLink());
|
|
};
|
|
// Fake - Character::OnLeaveField
|
|
{
|
|
// 자신을 포함한 인접섹터의 NPC들에게 pObject가 필드를 떠났음을 알려준다.
|
|
AI_MSG_LEAVE_FIELD msgAI;
|
|
msgAI.dwObjectKey = object_key;
|
|
pPlayer->SendToObservers( &msgAI, sizeof(msgAI) );
|
|
|
|
SummonManager::Instance()->LeaveSummonNPCs( pPlayer );
|
|
};
|
|
// Fake - Object::OnLeaveField
|
|
{
|
|
// Sector관련 처리밖에 없으므로 스킵
|
|
};
|
|
// Fake - Notification
|
|
{
|
|
MSG_CG_SYNC_PLAYER_LEAVE_BRD msgBRD;
|
|
msgBRD.m_byCount = 1;
|
|
msgBRD.m_PlayerKey[0] = (WORD)object_key;
|
|
|
|
m_pField->SendPacketAround( coordinates, &msgBRD, msgBRD.GetSize(), object_key );
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// #2 해당 플레이어를 옵저버 모드로 설정한다.
|
|
StatusManager* const pStatusManager = pPlayer->GetStatusManager();
|
|
StatusField& rStatus = pStatusManager->Status;
|
|
ISSQHandlerInGame& rHandler = pPlayer->SSQHandler;
|
|
{
|
|
pStatusManager->ChangeInterState( eCHAR_STATE_WAR_CTRL_OBSERVER_MODE, TRUE );
|
|
|
|
const Info::eCoordinates first_index =
|
|
(Info::eCoordinates)RandomNumberGenerator::GetRandomNumberRange( Info::eCoord_0, Info::eCoord_Slots );
|
|
rHandler->SetObserverCoordinateIndex( (BYTE)first_index );
|
|
// 아, 그러고 보니... 죽은 캐릭이어야 한당.
|
|
// 주변이 자신을 못보도록... npc도 마찬가지... 선택도 안되도록...
|
|
// TODO(WAVERIX)
|
|
|
|
const WzVector* const pPos = Observer::Controller::GetPosByIndex( first_index );
|
|
m_pField->TeleportObject( pPlayer, pPos, TRUE );
|
|
|
|
return TRUE;
|
|
};
|
|
}
|
|
|
|
|
|
//───────────────────────────────────────────────────────────
|
|
|
|
|
|
BOOLEAN
|
|
GameWarObserverMode::ChangeCoordinates(
|
|
const nsSSQ::Rules::eC2G_ReqRule rules
|
|
, Player* const pPlayer
|
|
, const nsSSQ::Observer::Info::eKeyCmd key_cmd )
|
|
{
|
|
using namespace nsGrid;
|
|
using namespace nsSSQ;
|
|
typedef nsSSQ::Observer::Info Info;
|
|
if( !m_pField )
|
|
return FALSE;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// #2 옵저버 모드의 해당 플레이어를 다른 좌표로 재설정한다.
|
|
StatusManager* const pStatusManager = pPlayer->GetStatusManager();
|
|
StatusField& rStatus = pStatusManager->Status;
|
|
ISSQHandlerInGame& rHandler = pPlayer->SSQHandler;
|
|
{
|
|
//if( !(rStatus.FIELD & rStatus.ENABLE_OBSERVER_MODE) )
|
|
// return;
|
|
|
|
const Info::eCoordinates current_index = (Info::eCoordinates)rHandler->GetObserverCoordinateIndex();
|
|
if( !((DWORD)current_index < (DWORD)Info::eCoord_Slots) )
|
|
return FALSE;
|
|
if( !((DWORD)key_cmd < (DWORD)Info::eKeyCmd_Max) )
|
|
return FALSE;
|
|
|
|
const Info::eCoordinates next_index = Observer::Controller::GetNextIndex( current_index, key_cmd );
|
|
rHandler->SetObserverCoordinateIndex( (BYTE)next_index );
|
|
|
|
const WzVector* const pPos = Observer::Controller::GetPosByIndex( next_index );
|
|
//printf( "set %.2f:%.2f:%.2f\n", pPos->x, pPos->y, pPos->z );
|
|
m_pField->TeleportObject( pPlayer, pPos, TRUE );
|
|
};
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|