mirror of
https://bitbucket.org/CPMADevs/cnq3
synced 2024-11-10 06:31:48 +00:00
589 lines
15 KiB
C++
589 lines
15 KiB
C++
|
/*
|
||
|
===========================================================================
|
||
|
Copyright (C) 1999-2005 Id Software, Inc.
|
||
|
|
||
|
This file is part of Quake III Arena source code.
|
||
|
|
||
|
Quake III Arena source code is free software; you can redistribute it
|
||
|
and/or modify it under the terms of the GNU General Public License as
|
||
|
published by the Free Software Foundation; either version 2 of the License,
|
||
|
or (at your option) any later version.
|
||
|
|
||
|
Quake III Arena source code is distributed in the hope that it will be
|
||
|
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License
|
||
|
along with Quake III Arena source code; if not, write to the Free Software
|
||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||
|
===========================================================================
|
||
|
*/
|
||
|
#include "tr_local.h"
|
||
|
|
||
|
|
||
|
// returns true if the grid is completely culled away.
|
||
|
// also sets the clipped hint bit in tess
|
||
|
|
||
|
static qbool R_CullTriSurf( const srfTriangles_t* cv )
|
||
|
{
|
||
|
return ( R_CullLocalBox( cv->bounds ) == CULL_OUT );
|
||
|
}
|
||
|
|
||
|
|
||
|
// returns true if the grid is completely culled away.
|
||
|
// also sets the clipped hint bit in tess
|
||
|
|
||
|
static qbool R_CullGrid( const srfGridMesh_t* cv )
|
||
|
{
|
||
|
int sphereCull;
|
||
|
|
||
|
if ( r_nocurves->integer ) {
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
if ( tr.currentEntityNum != ENTITYNUM_WORLD ) {
|
||
|
sphereCull = R_CullLocalPointAndRadius( cv->localOrigin, cv->meshRadius );
|
||
|
} else {
|
||
|
sphereCull = R_CullPointAndRadius( cv->localOrigin, cv->meshRadius );
|
||
|
}
|
||
|
|
||
|
// check for trivial reject
|
||
|
if ( sphereCull == CULL_OUT )
|
||
|
{
|
||
|
tr.pc[RF_BEZ_CULL_S_OUT]++;
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
// check bounding box if necessary
|
||
|
if ( sphereCull == CULL_CLIP )
|
||
|
{
|
||
|
tr.pc[RF_BEZ_CULL_S_CLIP]++;
|
||
|
|
||
|
int boxCull = R_CullLocalBox( cv->meshBounds );
|
||
|
|
||
|
if ( boxCull == CULL_OUT )
|
||
|
{
|
||
|
tr.pc[RF_BEZ_CULL_B_OUT]++;
|
||
|
return qtrue;
|
||
|
}
|
||
|
else if ( boxCull == CULL_IN )
|
||
|
{
|
||
|
tr.pc[RF_BEZ_CULL_B_IN]++;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
tr.pc[RF_BEZ_CULL_B_CLIP]++;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
tr.pc[RF_BEZ_CULL_S_IN]++;
|
||
|
}
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
|
||
|
// try to cull surfaces before they are added to the draw list
|
||
|
// this code will also allow mirrors on both sides of a model without recursion
|
||
|
|
||
|
static qbool R_CullSurface( const surfaceType_t* surface, const shader_t* shader )
|
||
|
{
|
||
|
if ( r_nocull->integer ) {
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
if ( *surface == SF_GRID ) {
|
||
|
return R_CullGrid( (const srfGridMesh_t*)surface );
|
||
|
}
|
||
|
|
||
|
if ( *surface == SF_TRIANGLES ) {
|
||
|
return R_CullTriSurf( (const srfTriangles_t*)surface );
|
||
|
}
|
||
|
|
||
|
if ( *surface != SF_FACE ) {
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
if ( shader->cullType == CT_TWO_SIDED ) {
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
const srfSurfaceFace_t* face = (const srfSurfaceFace_t*)surface;
|
||
|
float d = DotProduct( tr.or.viewOrigin, face->plane.normal );
|
||
|
|
||
|
// don't cull exactly on the plane, because there are levels of rounding
|
||
|
// through the BSP, ICD, and hardware that may cause pixel gaps if an
|
||
|
// epsilon isn't allowed here
|
||
|
if ( shader->cullType == CT_FRONT_SIDED ) {
|
||
|
if ( d < face->plane.dist - 8 ) {
|
||
|
return qtrue;
|
||
|
}
|
||
|
} else {
|
||
|
if ( d > face->plane.dist + 8 ) {
|
||
|
return qtrue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////
|
||
|
|
||
|
|
||
|
static qbool R_LightCullBounds( const dlight_t* dl, const vec3_t mins, const vec3_t maxs )
|
||
|
{
|
||
|
if (dl->transformed[0] - dl->radius > maxs[0])
|
||
|
return qtrue;
|
||
|
if (dl->transformed[0] + dl->radius < mins[0])
|
||
|
return qtrue;
|
||
|
|
||
|
if (dl->transformed[1] - dl->radius > maxs[1])
|
||
|
return qtrue;
|
||
|
if (dl->transformed[1] + dl->radius < mins[1])
|
||
|
return qtrue;
|
||
|
|
||
|
if (dl->transformed[2] - dl->radius > maxs[2])
|
||
|
return qtrue;
|
||
|
if (dl->transformed[2] + dl->radius < mins[2])
|
||
|
return qtrue;
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
|
||
|
static qbool R_LightCullFace( const srfSurfaceFace_t* face, const dlight_t* dl )
|
||
|
{
|
||
|
float d = DotProduct( dl->origin, face->plane.normal ) - face->plane.dist;
|
||
|
if ( (d < -dl->radius) || (d > dl->radius) )
|
||
|
return qtrue;
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
|
||
|
static qbool R_LightCullSurface( const surfaceType_t* surface, const dlight_t* dl )
|
||
|
{
|
||
|
switch (*surface) {
|
||
|
case SF_FACE:
|
||
|
return R_LightCullFace( (const srfSurfaceFace_t*)surface, dl );
|
||
|
case SF_GRID: {
|
||
|
const srfGridMesh_t* grid = (const srfGridMesh_t*)surface;
|
||
|
return R_LightCullBounds( dl, grid->meshBounds[0], grid->meshBounds[1] );
|
||
|
}
|
||
|
case SF_TRIANGLES: {
|
||
|
const srfTriangles_t* tris = (const srfTriangles_t*)surface;
|
||
|
return R_LightCullBounds( dl, tris->bounds[0], tris->bounds[1] );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return qfalse;
|
||
|
}
|
||
|
|
||
|
|
||
|
static void R_AddWorldSurface( msurface_t* surf )
|
||
|
{
|
||
|
if ( surf->vcBSP == tr.viewCount )
|
||
|
return; // already checked during this BSP walk
|
||
|
|
||
|
surf->vcBSP = tr.viewCount;
|
||
|
|
||
|
if ( R_CullSurface( surf->data, surf->shader ) )
|
||
|
return;
|
||
|
|
||
|
surf->vcVisible = tr.viewCount;
|
||
|
|
||
|
R_AddDrawSurf( surf->data, surf->shader, surf->fogIndex );
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
=============================================================
|
||
|
|
||
|
WORLD MODEL
|
||
|
|
||
|
=============================================================
|
||
|
*/
|
||
|
|
||
|
|
||
|
static void R_RecursiveWorldNode( mnode_t *node, int planeBits )
|
||
|
{
|
||
|
|
||
|
do {
|
||
|
// if the node wasn't marked as potentially visible, exit
|
||
|
if (node->visframe != tr.visCount)
|
||
|
return;
|
||
|
|
||
|
// if the bounding volume is completely outside the frustum, dump it
|
||
|
|
||
|
if ( !r_nocull->integer ) {
|
||
|
int r;
|
||
|
|
||
|
if ( planeBits & 1 ) {
|
||
|
r = BoxOnPlaneSide(node->mins, node->maxs, &tr.viewParms.frustum[0]);
|
||
|
if (r == 2) {
|
||
|
return; // culled
|
||
|
}
|
||
|
if ( r == 1 ) {
|
||
|
planeBits &= ~1; // all descendants will also be in front
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( planeBits & 2 ) {
|
||
|
r = BoxOnPlaneSide(node->mins, node->maxs, &tr.viewParms.frustum[1]);
|
||
|
if (r == 2) {
|
||
|
return; // culled
|
||
|
}
|
||
|
if ( r == 1 ) {
|
||
|
planeBits &= ~2; // all descendants will also be in front
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( planeBits & 4 ) {
|
||
|
r = BoxOnPlaneSide(node->mins, node->maxs, &tr.viewParms.frustum[2]);
|
||
|
if (r == 2) {
|
||
|
return; // culled
|
||
|
}
|
||
|
if ( r == 1 ) {
|
||
|
planeBits &= ~4; // all descendants will also be in front
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( planeBits & 8 ) {
|
||
|
r = BoxOnPlaneSide(node->mins, node->maxs, &tr.viewParms.frustum[3]);
|
||
|
if (r == 2) {
|
||
|
return; // culled
|
||
|
}
|
||
|
if ( r == 1 ) {
|
||
|
planeBits &= ~8; // all descendants will also be in front
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
if (node->contents != CONTENTS_NODE)
|
||
|
break;
|
||
|
|
||
|
// recurse down the children, front side first
|
||
|
R_RecursiveWorldNode( node->children[0], planeBits );
|
||
|
|
||
|
// tail recurse
|
||
|
node = node->children[1];
|
||
|
|
||
|
} while ( 1 );
|
||
|
|
||
|
// leaf node, so add mark surfaces
|
||
|
tr.pc[RF_LEAFS]++;
|
||
|
|
||
|
// add to z buffer bounds
|
||
|
if ( node->mins[0] < tr.viewParms.visBounds[0][0] ) {
|
||
|
tr.viewParms.visBounds[0][0] = node->mins[0];
|
||
|
}
|
||
|
if ( node->mins[1] < tr.viewParms.visBounds[0][1] ) {
|
||
|
tr.viewParms.visBounds[0][1] = node->mins[1];
|
||
|
}
|
||
|
if ( node->mins[2] < tr.viewParms.visBounds[0][2] ) {
|
||
|
tr.viewParms.visBounds[0][2] = node->mins[2];
|
||
|
}
|
||
|
|
||
|
if ( node->maxs[0] > tr.viewParms.visBounds[1][0] ) {
|
||
|
tr.viewParms.visBounds[1][0] = node->maxs[0];
|
||
|
}
|
||
|
if ( node->maxs[1] > tr.viewParms.visBounds[1][1] ) {
|
||
|
tr.viewParms.visBounds[1][1] = node->maxs[1];
|
||
|
}
|
||
|
if ( node->maxs[2] > tr.viewParms.visBounds[1][2] ) {
|
||
|
tr.viewParms.visBounds[1][2] = node->maxs[2];
|
||
|
}
|
||
|
|
||
|
// add the individual surfaces
|
||
|
int c = node->nummarksurfaces;
|
||
|
msurface_t** mark = node->firstmarksurface;
|
||
|
while (c--) {
|
||
|
// the surface may have already been added if it spans multiple leafs
|
||
|
msurface_t* surf = *mark;
|
||
|
R_AddWorldSurface( surf );
|
||
|
mark++;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////
|
||
|
|
||
|
|
||
|
static void R_AddLitSurface( msurface_t* surf, const dlight_t* light )
|
||
|
{
|
||
|
// since we're not worried about offscreen lights casting into the frustum (ATM !!!)
|
||
|
// only add the "lit" version of this surface if it was already added to the view
|
||
|
//if ( surf->viewCount != tr.viewCount )
|
||
|
// return;
|
||
|
|
||
|
// surfaces that were faceculled will still have the current viewCount in vcBSP
|
||
|
// because that's set to indicate that it's BEEN vis tested at all, to avoid
|
||
|
// repeated vis tests, not whether it actually PASSED the vis test or not
|
||
|
// only light surfaces that are GENUINELY visible, as opposed to merely in a visible LEAF
|
||
|
if ( surf->vcVisible != tr.viewCount )
|
||
|
return;
|
||
|
|
||
|
if ( surf->shader->surfaceFlags & (SURF_NODLIGHT | SURF_SKY) )
|
||
|
return;
|
||
|
|
||
|
if ( surf->shader->sort != SS_OPAQUE )
|
||
|
return;
|
||
|
|
||
|
if ( surf->lightCount == tr.lightCount )
|
||
|
return; // already in the lit list (or already culled) for this light
|
||
|
|
||
|
surf->lightCount = tr.lightCount;
|
||
|
|
||
|
if ( R_LightCullSurface( surf->data, light ) ) {
|
||
|
tr.pc[RF_LIT_CULLS]++;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
R_AddLitSurf( surf->data, surf->shader, surf->fogIndex );
|
||
|
}
|
||
|
|
||
|
|
||
|
static void R_RecursiveLightNode( mnode_t* node )
|
||
|
{
|
||
|
do {
|
||
|
// if the node wasn't marked as potentially visible, exit
|
||
|
if (node->visframe != tr.visCount)
|
||
|
return;
|
||
|
|
||
|
if (node->contents != CONTENTS_NODE)
|
||
|
break;
|
||
|
|
||
|
qbool children[2];
|
||
|
children[0] = children[1] = qfalse;
|
||
|
|
||
|
float d = DotProduct( tr.light->origin, node->plane->normal ) - node->plane->dist;
|
||
|
if ( d > -tr.light->radius ) {
|
||
|
children[0] = qtrue;
|
||
|
}
|
||
|
if ( d < tr.light->radius ) {
|
||
|
children[1] = qtrue;
|
||
|
}
|
||
|
|
||
|
if ( children[0] && children[1] ) {
|
||
|
R_RecursiveLightNode( node->children[0] );
|
||
|
node = node->children[1];
|
||
|
}
|
||
|
else if ( children[0] ) {
|
||
|
node = node->children[0];
|
||
|
}
|
||
|
else if ( children[1] ) {
|
||
|
node = node->children[1];
|
||
|
}
|
||
|
else {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
} while ( 1 );
|
||
|
|
||
|
tr.pc[RF_LIT_LEAFS]++;
|
||
|
|
||
|
// add the individual surfaces
|
||
|
int c = node->nummarksurfaces;
|
||
|
msurface_t** mark = node->firstmarksurface;
|
||
|
while (c--) {
|
||
|
// the surface may have already been added if it spans multiple leafs
|
||
|
msurface_t* surf = *mark;
|
||
|
R_AddLitSurface( surf, tr.light );
|
||
|
mark++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////
|
||
|
// BRUSH MODELS
|
||
|
|
||
|
|
||
|
void R_AddBrushModelSurfaces( const trRefEntity_t* re )
|
||
|
{
|
||
|
const model_t* model = R_GetModelByHandle( re->e.hModel );
|
||
|
const bmodel_t* bmodel = model->bmodel;
|
||
|
|
||
|
if ( R_CullLocalBox( bmodel->bounds ) == CULL_OUT )
|
||
|
return;
|
||
|
|
||
|
for ( int s = 0; s < bmodel->numSurfaces; ++s ) {
|
||
|
R_AddWorldSurface( bmodel->firstSurface + s );
|
||
|
}
|
||
|
|
||
|
R_TransformDlights( tr.refdef.num_dlights, tr.refdef.dlights, &tr.or );
|
||
|
|
||
|
for ( int i = 0; i < tr.refdef.num_dlights; ++i ) {
|
||
|
dlight_t* dl = &tr.refdef.dlights[i];
|
||
|
if (R_LightCullBounds( dl, bmodel->bounds[0], bmodel->bounds[1] ))
|
||
|
continue;
|
||
|
++tr.lightCount;
|
||
|
tr.light = dl;
|
||
|
for ( int s = 0; s < bmodel->numSurfaces; ++s ) {
|
||
|
R_AddLitSurface( bmodel->firstSurface + s, dl );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////
|
||
|
|
||
|
|
||
|
static mnode_t* R_PointInLeaf( const vec3_t p )
|
||
|
{
|
||
|
if ( !tr.world ) {
|
||
|
ri.Error( ERR_DROP, "R_PointInLeaf: no world" );
|
||
|
}
|
||
|
|
||
|
mnode_t* node = tr.world->nodes;
|
||
|
while (node->contents == CONTENTS_NODE) {
|
||
|
const cplane_t* plane = node->plane;
|
||
|
float d = DotProduct (p,plane->normal) - plane->dist;
|
||
|
if (d > 0) {
|
||
|
node = node->children[0];
|
||
|
} else {
|
||
|
node = node->children[1];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
|
||
|
static const byte* R_ClusterPVS( int cluster )
|
||
|
{
|
||
|
if ( !tr.world || !tr.world->vis || cluster < 0 || cluster >= tr.world->numClusters )
|
||
|
return tr.world->novis;
|
||
|
return tr.world->vis + cluster * tr.world->clusterBytes;
|
||
|
}
|
||
|
|
||
|
|
||
|
qbool R_inPVS( const vec3_t p1, const vec3_t p2 )
|
||
|
{
|
||
|
const mnode_t* leaf;
|
||
|
|
||
|
leaf = R_PointInLeaf( p1 );
|
||
|
const byte* vis = CM_ClusterPVS( leaf->cluster );
|
||
|
leaf = R_PointInLeaf( p2 );
|
||
|
|
||
|
if ( !(vis[leaf->cluster>>3] & (1<<(leaf->cluster&7))) ) {
|
||
|
return qfalse;
|
||
|
}
|
||
|
return qtrue;
|
||
|
}
|
||
|
|
||
|
|
||
|
// mark the leaves and nodes that are in the PVS for the current cluster
|
||
|
|
||
|
static void R_MarkLeaves()
|
||
|
{
|
||
|
mnode_t* leaf;
|
||
|
int i, cluster;
|
||
|
|
||
|
// lockpvs lets designers walk around to determine the
|
||
|
// extent of the current pvs
|
||
|
if ( r_lockpvs->integer ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// current viewcluster
|
||
|
leaf = R_PointInLeaf( tr.viewParms.pvsOrigin );
|
||
|
cluster = leaf->cluster;
|
||
|
|
||
|
tr.pc[RF_LEAF_CLUSTER] = cluster;
|
||
|
tr.pc[RF_LEAF_AREA] = leaf->area;
|
||
|
|
||
|
// if the cluster is the same and the area visibility matrix
|
||
|
// hasn't changed, we don't need to mark everything again
|
||
|
if ( tr.viewCluster == cluster && !tr.refdef.areamaskModified ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
tr.visCount++;
|
||
|
tr.viewCluster = cluster;
|
||
|
|
||
|
if ( r_novis->integer || tr.viewCluster == -1 ) {
|
||
|
for ( i = 0; i < tr.world->numnodes; ++i ) {
|
||
|
if (tr.world->nodes[i].contents != CONTENTS_SOLID) {
|
||
|
tr.world->nodes[i].visframe = tr.visCount;
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const byte* vis = R_ClusterPVS( tr.viewCluster );
|
||
|
|
||
|
for ( i = 0, leaf = tr.world->nodes; i < tr.world->numnodes; ++i, ++leaf ) {
|
||
|
cluster = leaf->cluster;
|
||
|
if ( cluster < 0 || cluster >= tr.world->numClusters ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check general pvs
|
||
|
if ( !(vis[cluster>>3] & (1<<(cluster&7))) ) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check for door connection
|
||
|
if ( (tr.refdef.areamask[leaf->area>>3] & (1<<(leaf->area&7)) ) ) {
|
||
|
continue; // not visible
|
||
|
}
|
||
|
|
||
|
mnode_t* parent = leaf;
|
||
|
do {
|
||
|
if (parent->visframe == tr.visCount)
|
||
|
break;
|
||
|
parent->visframe = tr.visCount;
|
||
|
parent = parent->parent;
|
||
|
} while (parent);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void R_AddWorldSurfaces()
|
||
|
{
|
||
|
if ( !r_drawworld->integer ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ( tr.refdef.rdflags & RDF_NOWORLDMODEL ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
tr.currentEntityNum = ENTITYNUM_WORLD;
|
||
|
tr.shiftedEntityNum = tr.currentEntityNum << QSORT_ENTITYNUM_SHIFT;
|
||
|
|
||
|
// determine which leaves are in the PVS / areamask
|
||
|
R_MarkLeaves();
|
||
|
|
||
|
// add all the visible surfaces and regenerate the visible min/max
|
||
|
ClearBounds( tr.viewParms.visBounds[0], tr.viewParms.visBounds[1] );
|
||
|
R_RecursiveWorldNode( tr.world->nodes, 15 );
|
||
|
|
||
|
if ( tr.refdef.num_dlights > MAX_DLIGHTS )
|
||
|
tr.refdef.num_dlights = MAX_DLIGHTS;
|
||
|
|
||
|
// "transform" all the dlights so that dl->transformed is actually populated
|
||
|
// (even though HERE it's == dl->origin) so we can always use R_LightCullBounds
|
||
|
// instead of having copypasted versions for both world and local cases
|
||
|
R_TransformDlights( tr.refdef.num_dlights, tr.refdef.dlights, &tr.viewParms.world );
|
||
|
|
||
|
for ( int i = 0; i < tr.refdef.num_dlights; ++i ) {
|
||
|
dlight_t* dl = &tr.refdef.dlights[i];
|
||
|
dl->head = dl->tail = 0;
|
||
|
if ( R_CullPointAndRadius( dl->origin, dl->radius ) == CULL_OUT ) {
|
||
|
tr.pc[RF_LIGHT_CULL_OUT]++;
|
||
|
continue;
|
||
|
}
|
||
|
tr.pc[RF_LIGHT_CULL_IN]++;
|
||
|
++tr.lightCount;
|
||
|
tr.light = dl;
|
||
|
R_RecursiveLightNode( tr.world->nodes );
|
||
|
}
|
||
|
}
|