SQCSimulator2023/SQCSim2021/engine.cpp
MarcEricMartel af0464b3a2 !
2021-12-01 21:22:27 -05:00

649 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "engine.h"
#include <algorithm>
#include <cmath>
#include "transformation.h"
#include "player.h"
Engine::Engine() { }
Engine::~Engine() { }
void Engine::Init() {
GLenum glewErr = glewInit();
if (glewErr != GLEW_OK) {
std::cerr << " ERREUR GLEW : " << glewGetErrorString(glewErr) << std::endl;
abort();
}
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_TEXTURE_2D);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, (float)Width() / (float)Height(), 0.0001f, 1000.0f);
glEnable(GL_DEPTH_TEST);
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glShadeModel(GL_SMOOTH);
glEnable(GL_LIGHTING);
glEnable(GL_LINE_SMOOTH);
glEnable(GL_CULL_FACE);
glDisable(GL_FRAMEBUFFER_SRGB);
glEnable(GL_BLEND);
glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA);
// Light
GLfloat light0Pos[4] = { 0.0f, CHUNK_SIZE_Y, 0.0f, 1.0f };
GLfloat light0Amb[4] = { 0.2f, 0.2f, 0.2f, 1.f };
GLfloat light0Diff[4] = { 1.f, 1.f, 1.f, 1.f };
GLfloat light0Spec[4] = { 0.2f, 0.2f, 0.2f, 1.0f };
glEnable(GL_LIGHT0);
glLightfv(GL_LIGHT0, GL_POSITION, light0Pos);
glLightfv(GL_LIGHT0, GL_AMBIENT, light0Amb);
glLightfv(GL_LIGHT0, GL_DIFFUSE, light0Diff);
glLightfv(GL_LIGHT0, GL_SPECULAR, light0Spec);
// Init manifeste de chunks renderés.
m_renderManifest.reserve(3000);
// Objet de skybox avec sa propre texture et son propre shader!
m_skybox.Init(0.00013f);
// Objet de musique!
//m_audio.ToggleMusicState();
// Init Chunks
m_world.GetChunks().Reset(nullptr);
// Gestion de souris.
CenterMouse();
HideCursor();
}
void Engine::DeInit() { }
void Engine::LoadResource() {
LoadTexture(m_textureFloor, TEXTURE_PATH "grass.png");
LoadTexture(m_skybox.GetTexture(), TEXTURE_PATH "skybox.png");
LoadTexture(m_textureCrosshair, TEXTURE_PATH "cross.bmp");
LoadTexture(m_textureFont, TEXTURE_PATH "font.bmp");
TextureAtlas::TextureIndex texDirtIndex = m_textureAtlas.AddTexture(TEXTURE_PATH "metal3.png");
TextureAtlas::TextureIndex texIceIndex = m_textureAtlas.AddTexture(TEXTURE_PATH "dirt.png");
TextureAtlas::TextureIndex texGrassIndex = m_textureAtlas.AddTexture(TEXTURE_PATH "grass.png");
TextureAtlas::TextureIndex texMetalIndex = m_textureAtlas.AddTexture(TEXTURE_PATH "metal.png");
if (!m_textureAtlas.Generate(512, false)) {
std::cout << " Unable to generate texture atlas ..." << std::endl;
abort();
}
float u, v, s;
m_textureAtlas.TextureIndexToCoord(texDirtIndex, u, v, s, s);
m_blockinfo[BTYPE_DIRT] = new BlockInfo(BTYPE_DIRT, "Dirt", u, v, s, 1);
m_textureAtlas.TextureIndexToCoord(texGrassIndex, u, v, s, s);
m_blockinfo[BTYPE_GRASS] = new BlockInfo(BTYPE_GRASS, "Grass", u, v, s, 1);
m_textureAtlas.TextureIndexToCoord(texMetalIndex, u, v, s, s);
m_blockinfo[BTYPE_METAL] = new BlockInfo(BTYPE_METAL, "Metal", u, v, s, 1);
m_textureAtlas.TextureIndexToCoord(texIceIndex, u, v, s, s);
m_blockinfo[BTYPE_ICE] = new BlockInfo(BTYPE_ICE, "Ice", u, v, s, 1);
std::cout << " Loading and compiling shaders ..." << std::endl;
if (!m_shader01.Load(SHADER_PATH "shader01.vert", SHADER_PATH "shader01.frag", true)) {
std::cout << " Failed to load shader " << std::endl;
exit(1);
}
if (!m_skybox.GetShader().Load(SHADER_PATH "skybox.vert", SHADER_PATH "skybox.frag", true)) {
std::cout << " Failed to load shader " << std::endl;
exit(1);
}
}
void Engine::UnloadResource() {}
void Engine::DrawHud(float elapsedTime) {
// Setter le blend function , tout ce qui sera noir sera transparent
glDisable(GL_LIGHTING);
glColor4f(1.f, 1.f, 1.f, 1.f);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
//glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho(0, Width(), 0, Height(), -1, 1);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
// Bind de la texture pour le font
m_textureFont.Bind();
std::ostringstream ss;
ss << " Fps : " << GetFps(elapsedTime);
PrintText(10, Height() - 25, ss.str());
ss.str("");
ss << " Rendered Chunks : " << m_renderCount;
PrintText(10, Height() - 35, ss.str());
ss.str("");
ss << " Bad Hits on Chunks : " << m_badHitCount;
PrintText(10, Height() - 45, ss.str());
ss.str("");
ss << " Velocity : " << m_player.GetVelocity(); // IMPORTANT : on utilise l operateur << pour afficher la position
PrintText(10, 10, ss.str());
ss.str("");
ss << " Direction : " << m_player.GetDirection();
PrintText(10, 20, ss.str());
ss.str("");
ss << " Position : " << m_player.GetPosition();
PrintText(10, 30, ss.str());
ss.str("");
ss << " CamPos : " << m_player.GetPOV();
PrintText(10, 40, ss.str());
// Affichage du crosshair
m_textureCrosshair.Bind();
static const int crossSize = 32;
glLoadIdentity();
glTranslated(Width() / 2 - crossSize / 2, Height() / 2 - crossSize / 2, 0);
glBegin(GL_QUADS);
glTexCoord2f(0, 0);
glVertex2i(0, 0);
glTexCoord2f(1, 0);
glVertex2i(crossSize, 0);
glTexCoord2f(1, 1);
glVertex2i(crossSize, crossSize);
glTexCoord2f(0, 1);
glVertex2i(0, crossSize);
glEnd();
glEnable(GL_LIGHTING);
//glDisable(GL_BLEND);
//glBlendFuncSeparate(GL_SRC_COLOR, GL_ONE_MINUS_DST_COLOR, GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA);
glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA);
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
}
void Engine::PrintText(unsigned int x, unsigned int y, const std::string& t) {
glLoadIdentity();
glTranslated(x, y, 0);
for (unsigned int i = 0; i < t.length(); ++i) {
float left = (float)((t[i] - 32) % 16) / 16.f;
float top = (float)((t[i] - 32) / 16) / 16.f;
top += .5f;
glBegin(GL_QUADS);
glTexCoord2f(left, 1.f - top - .0625f);
glVertex2f(0, 0);
glTexCoord2f(left + .0625f, 1.f - top - .0625f);
glVertex2f(12, 0);
glTexCoord2f(left + .0625f, 1.f - top);
glVertex2f(12, 12);
glTexCoord2f(left, 1.f - top);
glVertex2f(0, 12);
glEnd();
glTranslated(8, 0, 0);
}
}
int Engine::GetFps(float elapsedTime) const { return 1 / elapsedTime; }
void Engine::Render(float elapsedTime) {
static float gameTime = elapsedTime;
if (elapsedTime > 0.1f) return;
gameTime += elapsedTime;
Transformation all;
Transformation skybox;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Transformations initiales
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
m_player.ApplyPhysics(m_player.GetInput(m_keyW, m_keyS, m_keyA, m_keyD, m_keySpace, m_keylshift, elapsedTime), m_world, elapsedTime);
m_audio.Update3DAudio(m_player.GetPOV(), m_player.GetDirection(), m_player.GetVelocity()); // Ajustement du positionnement 3D avec les coordonnées du joueur et
// son vecteur de vélocité (pour l'effet Doppler)
m_player.ApplyTransformation(all);
m_player.ApplyTransformation(skybox, false); // Version d'ApplyTransformation qui ne tient compte que de la rotation
// (donc l'objet ne bouge pas relativement au joueur, ce qui est pratique pour une skybox!).
glDisable(GL_LIGHT0);
if (m_isSkybox) m_skybox.Render(skybox);
glEnable(GL_LIGHT0);
if (m_mouseL)
ChangeBlockAtCursor(BTYPE_DIRT);
else if (m_mouseR)
ChangeBlockAtCursor(BTYPE_AIR);
// Génération/Update des Chunks.
m_textureAtlas.Bind();
int cx = m_player.GetPosition().x;
int cy = m_player.GetPosition().z;
static int frameGenerate = 0;
static int frameUpdate = 0;
int side = 0;
if (frameGenerate > 0) --frameGenerate;
if (frameUpdate > 0) --frameUpdate;
if (!frameGenerate || !frameUpdate)
while (side * CHUNK_SIZE_X <= VIEW_DISTANCE * 2) {
int tx = -side, ty = -side;
for (; tx <= side; ++tx)
UpdateWorld(frameGenerate, frameUpdate, cx + tx * CHUNK_SIZE_X, cy + ty * CHUNK_SIZE_Z);
for (; ty <= side; ++ty)
UpdateWorld(frameGenerate, frameUpdate, cx + tx * CHUNK_SIZE_X, cy + ty * CHUNK_SIZE_Z);
for (; tx >= -side; --tx)
UpdateWorld(frameGenerate, frameUpdate, cx + tx * CHUNK_SIZE_X, cy + ty * CHUNK_SIZE_Z);
for (; ty >= -side; --ty)
UpdateWorld(frameGenerate, frameUpdate, cx + tx * CHUNK_SIZE_X, cy + ty * CHUNK_SIZE_Z);
++side;
}
// Rendering des Chunks.
m_shader01.Use();
m_renderCount = 0;
m_badHitCount = 0;
Vector3f angle;
Vector3f cursor;
Vector3f direct = m_player.GetDirection();
Vector3f pos = m_player.GetPosition() - direct;
direct.y = 0;
direct.Normalize();
pos.y = 1;
m_renderManifest.clear();
for (int dist = VIEW_DISTANCE; dist >= 0; dist -= CHUNK_SIZE_X) {
// Configuration du radar.
angle.x = direct.z + direct.x;
angle.y = 0;
angle.z = direct.z - direct.x;
angle.Normalize();
float sinus = .01745240643; // sin(1 degré)
float cosinus = .99984769515; // cos(1 degré)
int echantillons = 90;
for (int radar = 0; radar < echantillons; ++radar) {
float x = angle.x;
float z = angle.z;
angle.x = x * cosinus - z * sinus;
angle.z = z * cosinus + x * sinus;
angle.Normalize();
cursor = pos - direct * CHUNK_SIZE_X * 2 + angle * dist;
if (cursor.y >= 128.f || cursor.y >= 0.f) cursor.y = 1;
bool valide = true;
if (m_world.ChunkAt(cursor)) {
int chx, chy;
m_world.ChunkAt(cursor)->GetPosition(chx, chy);
for (int index = 0; index < m_renderManifest.size(); ++index)
if (m_renderManifest[index] == Vector3i(chx, 0, chy)) {
valide = false;
++m_badHitCount;
}
if (valide) {
all.ApplyTranslation(chx * CHUNK_SIZE_X, 0, chy * CHUNK_SIZE_Z);
all.Use();
float dist = (pos - cursor).Length();
float blend = ((float)VIEW_DISTANCE - dist * 2.f + 128.f) / (float)VIEW_DISTANCE;
glBlendColor(0.f,0.f,0.f,blend);
m_world.GetChunks().Get(chx, chy)->Render();
all.ApplyTranslation(-chx * CHUNK_SIZE_X, 0, -chy * CHUNK_SIZE_Z);
m_renderManifest.push_back(Vector3i(chx, 0, chy));
++m_renderCount;
}
}
}
}
m_shader01.Disable();
if (m_wireframe)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
DrawHud(elapsedTime);
if (m_wireframe)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
if (m_player.GetPosition().y < -20.f)
m_player = Player(Vector3f(0, CHUNK_SIZE_Y + 1.8f, 0)); // Respawn si le bonho- joueur tombe en bas du monde.
}
void Engine::KeyPressEvent(unsigned char key)
{
switch (key) {
case 36: // ESC
for (int x = 0; x < WORLD_SIZE_X; ++x) // Les destructeurs de Chunks ont de la misère je les aide un peu!
for (int y = 0; y < WORLD_SIZE_Y; ++y)
if (m_world.GetChunks().Get(x,y))
m_world.GetChunks().Get(x, y)->~Chunk();
m_world.GetChunks().Reset(nullptr); // Hack cheap qui empêche d'avoir une exception en sortant du jeu
Stop();
break;
case 94: // F10
SetFullscreen(!IsFullscreen());
break;
case 22: // W
if (!m_keyW) {
std::cout << "W " << std::endl;
m_keyW = true;
}
break;
case 0: // A
if (!m_keyA) {
std::cout << "A " << std::endl;
m_keyA = true;
}
break;
case 18: // S
if (!m_keyS) {
std::cout << "S " << std::endl;
m_keyS = true;
}
break;
case 3: // D
if (!m_keyD) {
std::cout << "D " << std::endl;
m_keyD = true;
}
break;
case 38: // Left Shift
if (!m_keylshift) {
std::cout << "Dash!" << std::endl;
m_keylshift = true;
}
break;
case 57: // Space
if (!m_keySpace) {
std::cout << "Jump! " << std::endl;
m_keySpace = true;
}
break;
case 24: // Y - Ignorer
case 255: // Fn - Ignorer
case 12: // M - Ignorer
case 17: // R - Ignorer
break;
default:
std::cout << "Unhandled key: " << (int)key << std::endl;
}
}
void Engine::KeyReleaseEvent(unsigned char key)
{
switch (key) {
case 12:
m_audio.ToggleMusicState();
break;
case 17:
m_isSkybox = !m_isSkybox;
break;
case 24: // Y
m_wireframe = !m_wireframe;
if (m_wireframe)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
else
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
break;
case 22: // W
std::cout << "rW " << std::endl;
m_keyW = false;
break;
case 0: // A
std::cout << "rA " << std::endl;
m_keyA = false;
break;
case 18: // S
std::cout << "rS " << std::endl;
m_keyS = false;
break;
case 3: // D
std::cout << "rD " << std::endl;
m_keyD = false;
break;
case 38: // Left Shift
std::cout << "rLS " << std::endl;
m_keylshift = false;
case 57: // Espace
std::cout << "rSpace " << std::endl;
m_keySpace = false;
break;
}
}
void Engine::MouseMoveEvent(int x, int y)
{
m_player.TurnLeftRight(x - (Width() / 2));
m_player.TurnTopBottom(y - (Height() / 2));
// Centrer la souris seulement si elle n'est pas déjà centrée
// Il est nécessaire de faire la vérification pour éviter de tomber
// dans une boucle infinie où l'appel à CenterMouse génère un
// MouseMoveEvent, qui rapelle CenterMouse qui rapelle un autre
// MouseMoveEvent, etc
if (x == (Width() / 2) && y == (Height() / 2))
return;
CenterMouse();
}
void Engine::MousePressEvent(const MOUSE_BUTTON& button, int x, int y) {
switch (button) {
case MOUSE_BUTTON_LEFT:
m_mouseL = true;
break;
case MOUSE_BUTTON_RIGHT:
m_mouseR = true;
break;
case MOUSE_BUTTON_MIDDLE:
m_mouseC = true;
break;
case MOUSE_BUTTON_WHEEL_UP:
m_mouseWU = true;
break;
case MOUSE_BUTTON_WHEEL_DOWN:
m_mouseWD = true;
break;
case MOUSE_BUTTON_NONE: break;
}
}
void Engine::MouseReleaseEvent(const MOUSE_BUTTON& button, int x, int y) {
switch (button) {
case MOUSE_BUTTON_LEFT:
m_mouseL = false;
m_block = false;
break;
case MOUSE_BUTTON_RIGHT:
m_mouseR = false;
m_block = false;
break;
case MOUSE_BUTTON_MIDDLE:
m_mouseC = false;
break;
case MOUSE_BUTTON_WHEEL_UP:
m_mouseWU = false;
break;
case MOUSE_BUTTON_WHEEL_DOWN:
m_mouseWD = false;
break;
case MOUSE_BUTTON_NONE: break;
}
}
bool Engine::LoadTexture(Texture& texture, const std::string& filename, bool stopOnError)
{
texture.Load(filename);
if (!texture.IsValid())
{
std::cerr << "Unable to load texture (" << filename << ")" << std::endl;
if (stopOnError)
Stop();
return false;
}
return true;
}
bool Engine::GenerateChunk(int chx, int chy) {
if (chx < WORLD_SIZE_X * CHUNK_SIZE_X && chy < WORLD_SIZE_Y * CHUNK_SIZE_Z &&
chx >= 0 && chy >= 0)
if (!m_world.ChunkAt(chx, 1, chy)) {
std::ostringstream pos;
pos << CHUNK_PATH << chx / CHUNK_SIZE_X << '_' << chy / CHUNK_SIZE_Z << ".chunk";
std::ifstream input(pos.str().c_str(), std::fstream::binary);
if (input.fail()) {
m_world.GetChunks().Set(chx / CHUNK_SIZE_X, chy / CHUNK_SIZE_Z, new Chunk(chx / CHUNK_SIZE_X, chy / CHUNK_SIZE_Z));
Chunk* chunk = m_world.GetChunks().Get(chx / CHUNK_SIZE_X, chy / CHUNK_SIZE_Z);
for (int x = 0; x < CHUNK_SIZE_X; ++x)
for (int z = 0; z < CHUNK_SIZE_Z; ++z) {
Vector3f perlin;
perlin.z = x * CHUNK_SIZE_X + CHUNK_SIZE_X * chx;
perlin.y = 0;
perlin.x = z * CHUNK_SIZE_Z + CHUNK_SIZE_Z * chy;
perlin.Normalize();
float height = m_perlin.Get(perlin.x, perlin.z) * 3 - 32;
for (int y = 0; y <= (int)height % CHUNK_SIZE_Y; ++y) {
chunk->SetBlock(x, y, z, BTYPE_METAL, &m_world);
}
}
for (int x = 0; x < CHUNK_SIZE_X; ++x)
for (int z = 0; z < CHUNK_SIZE_Z; ++z) {
Vector3f perlin;
perlin.x = x * CHUNK_SIZE_X + CHUNK_SIZE_X * chx;
perlin.y = 0;
perlin.z = z * CHUNK_SIZE_Z + CHUNK_SIZE_Z * chy;
perlin.Normalize();
float height = m_perlin.Get(perlin.x, perlin.z) + 16;
for (int y = 0; y <= (int)height % CHUNK_SIZE_Y; ++y) {
if (chunk->GetBlock(x, y, z) == BTYPE_AIR)
chunk->SetBlock(x, y, z, BTYPE_GRASS, &m_world);
}
}
for (int x = 0; x < CHUNK_SIZE_X; ++x)
for (int z = 0; z < CHUNK_SIZE_Z; ++z) {
for (int y = 0; y <= 10; ++y) {
if (chunk->GetBlock(x, y, z) == BTYPE_AIR)
chunk->SetBlock(x, y, z, BTYPE_ICE, &m_world);
}
}
for (int x = 0; x < CHUNK_SIZE_X; ++x)
for (int z = 0; z < CHUNK_SIZE_Z; ++z) {
for (int y = 0; y < CHUNK_SIZE_Y; ++y) {
Vector3f perlin;
perlin.x = x * CHUNK_SIZE_X + CHUNK_SIZE_X * chx;
perlin.y = (x + z) * CHUNK_SIZE_Y;
perlin.z = z * CHUNK_SIZE_Z + CHUNK_SIZE_Z * chy;
perlin.Normalize();
float height = m_perlin.Get(perlin.x, perlin.y, perlin.z);
if (chunk->GetBlock(x, y, z) != BTYPE_AIR && height > 18)
chunk->SetBlock(x, y, z, BTYPE_DIRT, &m_world);
}
}
}
else {
input.seekg(0, std::ios_base::end);
int size = input.tellg();
input.seekg(0, std::ios_base::beg);
char* data = new char[size];
input.read(data, size);
input.close();
m_world.GetChunks().Set(chx / CHUNK_SIZE_X, chy / CHUNK_SIZE_Z, new Chunk(chx / CHUNK_SIZE_X, chy / CHUNK_SIZE_Z, data));
delete[] data;
}
std::cout << "Chunk generated: " << chx / CHUNK_SIZE_X << ", " << chy / CHUNK_SIZE_Z << std::endl;
return true;
}
return false;
}
void Engine::UpdateWorld(int& generates, int& updates, int chx, int chy) {
if (generates == 0 && GenerateChunk(chx, chy)) generates = FRAMES_RENDER_CHUNKS;
if (updates == 0 && m_world.ChunkAt(chx, 1, chy) &&
m_world.ChunkAt(chx, 1, chy)->IsDirty()) {
m_world.ChunkAt(chx, 1, chy)->Update(m_blockinfo, &m_world);
updates = FRAMES_UPDATE_CHUNKS;
}
}
void Engine::ChangeBlockAtCursor(BlockType blockType) {
Vector3f currentPos = m_player.GetPosition();
Vector3f currentBlock = currentPos;
Vector3f ray = m_player.GetDirection();
bool found = false;
if (m_block) return;
while ((currentPos - currentBlock).Length() <= MAX_SELECTION_DISTANCE && !found) {
currentBlock += ray / 10.f;
BlockType bt = m_world.BlockAt(currentBlock);
if (bt != BTYPE_AIR)
found = true;
}
if (found) {
if (blockType != BTYPE_AIR) {
found = false;
while ((currentPos - currentBlock).Length() >= 1.7f && !found) {
currentBlock -= ray / 10.f;
BlockType bt = m_world.BlockAt(currentBlock);
if (bt == BTYPE_AIR) { // Vérification pour être sûr que le bloc à changer n'est pas dans le joueur.
int Bx = (int)currentBlock.x;
int By = (int)currentBlock.y;
int Bz = (int)currentBlock.z;
int Px = (int)currentPos.x;
int PyA = (int)currentPos.y;
int PyB = (int)(currentPos.y - .9f);
int PyC = (int)(currentPos.y - 1.7f);
int Pz = (int)currentPos.z;
if (!(Bx == Px &&
(By == PyA ||
By == PyB ||
By == PyC) &&
Bz == Pz))
found = true;
}
}
}
}
if (found && (int)currentBlock.y < CHUNK_SIZE_Y) {
int bx = (int)currentBlock.x % CHUNK_SIZE_X;
int by = (int)currentBlock.y % CHUNK_SIZE_Y;
int bz = (int)currentBlock.z % CHUNK_SIZE_Z;
m_world.ChunkAt(currentBlock)->SetBlock(bx, by, bz, blockType, &m_world);
m_world.ChunkAt(currentBlock)->MakeModified();
m_block = true;
}
}