2016-09-14 18:01:13 +00:00
|
|
|
//
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
//
|
|
|
|
// Copyright(C) 2010-2016 Christoph Oelckers
|
|
|
|
// All rights reserved.
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program 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 Lesser General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
|
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
|
|
|
//
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//
|
2013-06-23 07:49:34 +00:00
|
|
|
/*
|
|
|
|
** gl_voxels.cpp
|
|
|
|
**
|
|
|
|
** Voxel management
|
|
|
|
**
|
2016-09-14 18:01:13 +00:00
|
|
|
**/
|
2013-06-23 07:49:34 +00:00
|
|
|
|
|
|
|
#include "gl/system/gl_system.h"
|
|
|
|
#include "w_wad.h"
|
|
|
|
#include "cmdlib.h"
|
|
|
|
#include "sc_man.h"
|
|
|
|
#include "m_crc32.h"
|
|
|
|
#include "c_console.h"
|
|
|
|
#include "g_game.h"
|
|
|
|
#include "doomstat.h"
|
|
|
|
#include "g_level.h"
|
2013-09-28 21:33:55 +00:00
|
|
|
#include "colormatcher.h"
|
2013-06-23 07:49:34 +00:00
|
|
|
#include "textures/bitmap.h"
|
|
|
|
//#include "gl/gl_intern.h"
|
|
|
|
|
2013-09-03 16:29:39 +00:00
|
|
|
#include "gl/system/gl_interface.h"
|
2013-06-23 07:49:34 +00:00
|
|
|
#include "gl/renderer/gl_renderer.h"
|
|
|
|
#include "gl/data/gl_vertexbuffer.h"
|
|
|
|
#include "gl/scene/gl_drawinfo.h"
|
|
|
|
#include "gl/models/gl_models.h"
|
|
|
|
#include "gl/textures/gl_material.h"
|
|
|
|
#include "gl/utility/gl_geometric.h"
|
|
|
|
#include "gl/utility/gl_convert.h"
|
|
|
|
#include "gl/renderer/gl_renderstate.h"
|
|
|
|
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// Creates a 16x16 texture from the palette so that we can
|
|
|
|
// use the existing palette manipulation code to render the voxel
|
|
|
|
// Otherwise all shaders had to be duplicated and the non-shader code
|
|
|
|
// would be a lot less efficient.
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
class FVoxelTexture : public FTexture
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
|
|
|
|
FVoxelTexture(FVoxel *voxel);
|
|
|
|
~FVoxelTexture();
|
2017-03-09 18:54:41 +00:00
|
|
|
const uint8_t *GetColumn (unsigned int column, const Span **spans_out);
|
|
|
|
const uint8_t *GetPixels ();
|
2013-06-23 07:49:34 +00:00
|
|
|
void Unload ();
|
|
|
|
|
|
|
|
int CopyTrueColorPixels(FBitmap *bmp, int x, int y, int rotate, FCopyInfo *inf);
|
|
|
|
bool UseBasePalette() { return false; }
|
|
|
|
|
|
|
|
protected:
|
|
|
|
FVoxel *SourceVox;
|
2017-03-09 18:54:41 +00:00
|
|
|
uint8_t *Pixels;
|
2013-06-23 07:49:34 +00:00
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FVoxelTexture::FVoxelTexture(FVoxel *vox)
|
|
|
|
{
|
|
|
|
SourceVox = vox;
|
|
|
|
Width = 16;
|
|
|
|
Height = 16;
|
|
|
|
WidthBits = 4;
|
|
|
|
HeightBits = 4;
|
|
|
|
WidthMask = 15;
|
2013-09-28 21:33:55 +00:00
|
|
|
Pixels = NULL;
|
2013-06-23 07:49:34 +00:00
|
|
|
gl_info.bNoFilter = true;
|
|
|
|
gl_info.bNoCompress = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FVoxelTexture::~FVoxelTexture()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2017-03-09 18:54:41 +00:00
|
|
|
const uint8_t *FVoxelTexture::GetColumn (unsigned int column, const Span **spans_out)
|
2013-06-23 07:49:34 +00:00
|
|
|
{
|
|
|
|
// not needed
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2017-03-09 18:54:41 +00:00
|
|
|
const uint8_t *FVoxelTexture::GetPixels ()
|
2013-06-23 07:49:34 +00:00
|
|
|
{
|
2013-09-28 21:33:55 +00:00
|
|
|
// GetPixels gets called when a translated palette is used so we still need to implement it here.
|
|
|
|
if (Pixels == NULL)
|
|
|
|
{
|
2017-03-09 18:54:41 +00:00
|
|
|
Pixels = new uint8_t[256];
|
2013-09-28 21:33:55 +00:00
|
|
|
|
2017-03-09 18:54:41 +00:00
|
|
|
uint8_t *pp = SourceVox->Palette;
|
2013-09-28 21:33:55 +00:00
|
|
|
|
|
|
|
if(pp != NULL)
|
|
|
|
{
|
|
|
|
for(int i=0;i<256;i++, pp+=3)
|
|
|
|
{
|
|
|
|
PalEntry pe;
|
|
|
|
pe.r = (pp[0] << 2) | (pp[0] >> 4);
|
|
|
|
pe.g = (pp[1] << 2) | (pp[1] >> 4);
|
|
|
|
pe.b = (pp[2] << 2) | (pp[2] >> 4);
|
|
|
|
Pixels[i] = ColorMatcher.Pick(pe);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for(int i=0;i<256;i++, pp+=3)
|
|
|
|
{
|
2017-03-09 18:54:41 +00:00
|
|
|
Pixels[i] = (uint8_t)i;
|
2013-09-28 21:33:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Pixels;
|
2013-06-23 07:49:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void FVoxelTexture::Unload ()
|
|
|
|
{
|
2013-09-28 21:33:55 +00:00
|
|
|
if (Pixels != NULL)
|
|
|
|
{
|
|
|
|
delete[] Pixels;
|
|
|
|
Pixels = NULL;
|
|
|
|
}
|
2013-06-23 07:49:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// FVoxelTexture::CopyTrueColorPixels
|
|
|
|
//
|
|
|
|
// This creates a dummy 16x16 paletted bitmap and converts that using the
|
|
|
|
// voxel palette
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
int FVoxelTexture::CopyTrueColorPixels(FBitmap *bmp, int x, int y, int rotate, FCopyInfo *inf)
|
|
|
|
{
|
|
|
|
PalEntry pe[256];
|
2017-03-09 18:54:41 +00:00
|
|
|
uint8_t bitmap[256];
|
|
|
|
uint8_t *pp = SourceVox->Palette;
|
2013-06-23 07:49:34 +00:00
|
|
|
|
2013-09-28 21:33:55 +00:00
|
|
|
if(pp != NULL)
|
|
|
|
{
|
|
|
|
for(int i=0;i<256;i++, pp+=3)
|
|
|
|
{
|
2017-03-09 18:54:41 +00:00
|
|
|
bitmap[i] = (uint8_t)i;
|
2013-09-28 21:33:55 +00:00
|
|
|
pe[i].r = (pp[0] << 2) | (pp[0] >> 4);
|
|
|
|
pe[i].g = (pp[1] << 2) | (pp[1] >> 4);
|
|
|
|
pe[i].b = (pp[2] << 2) | (pp[2] >> 4);
|
|
|
|
pe[i].a = 255;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
2013-06-23 07:49:34 +00:00
|
|
|
{
|
2013-09-28 21:33:55 +00:00
|
|
|
for(int i=0;i<256;i++, pp+=3)
|
|
|
|
{
|
2017-03-09 18:54:41 +00:00
|
|
|
bitmap[i] = (uint8_t)i;
|
2013-09-28 21:33:55 +00:00
|
|
|
pe[i] = GPalette.BaseColors[i];
|
|
|
|
pe[i].a = 255;
|
|
|
|
}
|
|
|
|
}
|
2013-06-23 07:49:34 +00:00
|
|
|
bmp->CopyPixelData(x, y, bitmap, Width, Height, 1, 16, rotate, pe, inf);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FVoxelModel::FVoxelModel(FVoxel *voxel, bool owned)
|
|
|
|
{
|
|
|
|
mVoxel = voxel;
|
|
|
|
mOwningVoxel = owned;
|
2016-05-03 13:45:21 +00:00
|
|
|
mPalette = TexMan.AddTexture(new FVoxelTexture(voxel));
|
2013-06-23 07:49:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
FVoxelModel::~FVoxelModel()
|
|
|
|
{
|
|
|
|
if (mOwningVoxel) delete mVoxel;
|
|
|
|
}
|
|
|
|
|
2014-06-19 20:24:33 +00:00
|
|
|
|
2013-06-23 07:49:34 +00:00
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2014-06-19 20:24:33 +00:00
|
|
|
unsigned int FVoxelModel::AddVertex(FModelVertex &vert, FVoxelMap &check)
|
2013-06-23 07:49:34 +00:00
|
|
|
{
|
|
|
|
unsigned int index = check[vert];
|
|
|
|
if (index == 0xffffffff)
|
|
|
|
{
|
|
|
|
index = check[vert] =mVertices.Push(vert);
|
|
|
|
}
|
2014-06-19 20:24:33 +00:00
|
|
|
return index;
|
2013-06-23 07:49:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2017-03-09 18:54:41 +00:00
|
|
|
void FVoxelModel::AddFace(int x1, int y1, int z1, int x2, int y2, int z2, int x3, int y3, int z3, int x4, int y4, int z4, uint8_t col, FVoxelMap &check)
|
2013-06-23 07:49:34 +00:00
|
|
|
{
|
2016-03-24 15:10:17 +00:00
|
|
|
float PivotX = mVoxel->Mips[0].Pivot.X;
|
|
|
|
float PivotY = mVoxel->Mips[0].Pivot.Y;
|
|
|
|
float PivotZ = mVoxel->Mips[0].Pivot.Z;
|
2013-06-23 07:49:34 +00:00
|
|
|
int h = mVoxel->Mips[0].SizeZ;
|
2014-06-19 20:24:33 +00:00
|
|
|
FModelVertex vert;
|
|
|
|
unsigned int indx[4];
|
2013-06-23 07:49:34 +00:00
|
|
|
|
2016-10-03 14:09:32 +00:00
|
|
|
vert.packedNormal = 0; // currently this is not being used for voxels.
|
2013-06-23 07:49:34 +00:00
|
|
|
vert.u = (((col & 15) * 255 / 16) + 7) / 255.f;
|
|
|
|
vert.v = (((col / 16) * 255 / 16) + 7) / 255.f;
|
|
|
|
|
|
|
|
vert.x = x1 - PivotX;
|
|
|
|
vert.z = -y1 + PivotY;
|
|
|
|
vert.y = -z1 + PivotZ;
|
2014-06-19 20:24:33 +00:00
|
|
|
indx[0] = AddVertex(vert, check);
|
2013-06-23 07:49:34 +00:00
|
|
|
|
|
|
|
vert.x = x2 - PivotX;
|
|
|
|
vert.z = -y2 + PivotY;
|
|
|
|
vert.y = -z2 + PivotZ;
|
2014-06-19 20:24:33 +00:00
|
|
|
indx[1] = AddVertex(vert, check);
|
2013-06-23 07:49:34 +00:00
|
|
|
|
|
|
|
vert.x = x4 - PivotX;
|
|
|
|
vert.z = -y4 + PivotY;
|
|
|
|
vert.y = -z4 + PivotZ;
|
2014-06-19 20:24:33 +00:00
|
|
|
indx[2] = AddVertex(vert, check);
|
2013-06-23 07:49:34 +00:00
|
|
|
|
|
|
|
vert.x = x3 - PivotX;
|
|
|
|
vert.z = -y3 + PivotY;
|
|
|
|
vert.y = -z3 + PivotZ;
|
2014-06-19 20:24:33 +00:00
|
|
|
indx[3] = AddVertex(vert, check);
|
|
|
|
|
2016-10-03 14:09:32 +00:00
|
|
|
|
2014-06-19 20:24:33 +00:00
|
|
|
mIndices.Push(indx[0]);
|
|
|
|
mIndices.Push(indx[1]);
|
2014-06-30 16:57:24 +00:00
|
|
|
mIndices.Push(indx[3]);
|
2014-06-19 20:24:33 +00:00
|
|
|
mIndices.Push(indx[1]);
|
|
|
|
mIndices.Push(indx[2]);
|
2016-09-22 08:28:14 +00:00
|
|
|
mIndices.Push(indx[3]);
|
2013-06-23 07:49:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
void FVoxelModel::MakeSlabPolys(int x, int y, kvxslab_t *voxptr, FVoxelMap &check)
|
|
|
|
{
|
2017-03-09 18:54:41 +00:00
|
|
|
const uint8_t *col = voxptr->col;
|
2013-06-23 07:49:34 +00:00
|
|
|
int zleng = voxptr->zleng;
|
|
|
|
int ztop = voxptr->ztop;
|
|
|
|
int cull = voxptr->backfacecull;
|
|
|
|
|
|
|
|
if (cull & 16)
|
|
|
|
{
|
|
|
|
AddFace(x, y, ztop, x+1, y, ztop, x, y+1, ztop, x+1, y+1, ztop, *col, check);
|
|
|
|
}
|
|
|
|
int z = ztop;
|
|
|
|
while (z < ztop+zleng)
|
|
|
|
{
|
|
|
|
int c = 0;
|
|
|
|
while (z+c < ztop+zleng && col[c] == col[0]) c++;
|
|
|
|
|
|
|
|
if (cull & 1)
|
|
|
|
{
|
|
|
|
AddFace(x, y, z, x, y+1, z, x, y, z+c, x, y+1, z+c, *col, check);
|
|
|
|
}
|
|
|
|
if (cull & 2)
|
|
|
|
{
|
|
|
|
AddFace(x+1, y+1, z, x+1, y, z, x+1, y+1, z+c, x+1, y, z+c, *col, check);
|
|
|
|
}
|
|
|
|
if (cull & 4)
|
|
|
|
{
|
2016-10-11 07:08:31 +00:00
|
|
|
AddFace(x+1, y, z, x, y, z, x+1, y, z+c, x, y, z+c, *col, check);
|
2013-06-23 07:49:34 +00:00
|
|
|
}
|
|
|
|
if (cull & 8)
|
|
|
|
{
|
2016-10-11 07:08:31 +00:00
|
|
|
AddFace(x, y+1, z, x+1, y+1, z, x, y+1, z+c, x+1, y+1, z+c, *col, check);
|
|
|
|
}
|
2013-06-23 07:49:34 +00:00
|
|
|
z+=c;
|
|
|
|
col+=c;
|
|
|
|
}
|
|
|
|
if (cull & 32)
|
|
|
|
{
|
|
|
|
int z = ztop+zleng-1;
|
|
|
|
AddFace(x+1, y, z+1, x, y, z+1, x+1, y+1, z+1, x, y+1, z+1, voxptr->col[zleng-1], check);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
void FVoxelModel::Initialize()
|
|
|
|
{
|
|
|
|
FVoxelMap check;
|
|
|
|
FVoxelMipLevel *mip = &mVoxel->Mips[0];
|
|
|
|
for (int x = 0; x < mip->SizeX; x++)
|
|
|
|
{
|
2017-03-09 18:54:41 +00:00
|
|
|
uint8_t *slabxoffs = &mip->SlabData[mip->OffsetX[x]];
|
2013-06-23 07:49:34 +00:00
|
|
|
short *xyoffs = &mip->OffsetXY[x * (mip->SizeY + 1)];
|
|
|
|
for (int y = 0; y < mip->SizeY; y++)
|
|
|
|
{
|
|
|
|
kvxslab_t *voxptr = (kvxslab_t *)(slabxoffs + xyoffs[y]);
|
|
|
|
kvxslab_t *voxend = (kvxslab_t *)(slabxoffs + xyoffs[y+1]);
|
2017-03-09 18:54:41 +00:00
|
|
|
for (; voxptr < voxend; voxptr = (kvxslab_t *)((uint8_t *)voxptr + voxptr->zleng + 3))
|
2013-06-23 07:49:34 +00:00
|
|
|
{
|
|
|
|
MakeSlabPolys(x, y, voxptr, check);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-24 09:43:25 +00:00
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
void FVoxelModel::BuildVertexBuffer()
|
|
|
|
{
|
|
|
|
if (mVBuf == NULL)
|
|
|
|
{
|
|
|
|
Initialize();
|
|
|
|
|
2016-05-03 11:10:00 +00:00
|
|
|
mVBuf = new FModelVertexBuffer(true, true);
|
2014-10-24 09:43:25 +00:00
|
|
|
FModelVertex *vertptr = mVBuf->LockVertexBuffer(mVertices.Size());
|
|
|
|
unsigned int *indxptr = mVBuf->LockIndexBuffer(mIndices.Size());
|
|
|
|
|
|
|
|
memcpy(vertptr, &mVertices[0], sizeof(FModelVertex)* mVertices.Size());
|
|
|
|
memcpy(indxptr, &mIndices[0], sizeof(unsigned int)* mIndices.Size());
|
|
|
|
|
|
|
|
mVBuf->UnlockVertexBuffer();
|
|
|
|
mVBuf->UnlockIndexBuffer();
|
2014-10-24 09:52:45 +00:00
|
|
|
mNumIndices = mIndices.Size();
|
2014-10-24 09:43:25 +00:00
|
|
|
|
|
|
|
// delete our temporary buffers
|
|
|
|
mVertices.Clear();
|
|
|
|
mIndices.Clear();
|
|
|
|
mVertices.ShrinkToFit();
|
|
|
|
mIndices.ShrinkToFit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-05-03 16:14:16 +00:00
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// for skin precaching
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2017-03-09 18:54:41 +00:00
|
|
|
void FVoxelModel::AddSkins(uint8_t *hitlist)
|
2016-05-03 16:14:16 +00:00
|
|
|
{
|
|
|
|
hitlist[mPalette.GetIndex()] |= FTexture::TEX_Flat;
|
|
|
|
}
|
|
|
|
|
2013-06-23 07:49:34 +00:00
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
bool FVoxelModel::Load(const char * fn, int lumpnum, const char * buffer, int length)
|
|
|
|
{
|
|
|
|
return false; // not needed
|
|
|
|
}
|
|
|
|
|
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// Voxels don't have frames so always return 0
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
int FVoxelModel::FindFrame(const char * name)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2014-12-31 11:53:29 +00:00
|
|
|
//===========================================================================
|
|
|
|
//
|
|
|
|
// Voxels need aspect ratio correction according to the current map's setting
|
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
|
|
|
float FVoxelModel::getAspectFactor()
|
|
|
|
{
|
|
|
|
return glset.pixelstretch;
|
|
|
|
}
|
|
|
|
|
2013-06-23 07:49:34 +00:00
|
|
|
//===========================================================================
|
|
|
|
//
|
2014-06-19 20:24:33 +00:00
|
|
|
// Voxels never interpolate between frames, they only have one.
|
2013-06-23 07:49:34 +00:00
|
|
|
//
|
|
|
|
//===========================================================================
|
|
|
|
|
2014-06-19 11:58:49 +00:00
|
|
|
void FVoxelModel::RenderFrame(FTexture * skin, int frame, int frame2, double inter, int translation)
|
2013-06-23 07:49:34 +00:00
|
|
|
{
|
2014-08-22 21:50:38 +00:00
|
|
|
FMaterial * tex = FMaterial::ValidateTexture(skin, false);
|
2014-09-09 10:00:42 +00:00
|
|
|
gl_RenderState.SetMaterial(tex, CLAMP_NOFILTER, translation, -1, false);
|
2013-06-23 07:49:34 +00:00
|
|
|
|
2014-06-19 12:46:55 +00:00
|
|
|
gl_RenderState.Apply();
|
2016-05-03 11:10:00 +00:00
|
|
|
mVBuf->SetupFrame(0, 0, 0);
|
2014-10-24 09:43:25 +00:00
|
|
|
glDrawElements(GL_TRIANGLES, mNumIndices, GL_UNSIGNED_INT, (void*)(intptr_t)0);
|
2013-06-23 07:49:34 +00:00
|
|
|
}
|
|
|
|
|