Ejemplo n.º 1
0
void debug_stacks_init()
{
	g_debug_stacks_mutex.Init();
}
Ejemplo n.º 2
0
int main()
{
	sockets_init();
	atexit(sockets_cleanup);

	/*
		Run unit tests
	*/
	if(ENABLE_TESTS)
		run_tests();
	
	//return 0; //DEBUG
		
	/*
		Initialization
	*/

	srand(time(0));

	g_viewing_range_nodes_mutex.Init();
	assert(g_viewing_range_nodes_mutex.IsInitialized());

	MyEventReceiver receiver;

	// create device and exit if creation failed

	/*
		Host selection
	*/
	char connect_name[100];
	std::cout<<std::endl<<std::endl;
	std::cout<<"Address to connect to [empty = host a game]: ";
	std::cin.getline(connect_name, 100);
	
	bool hosting = false;
	if(connect_name[0] == 0){
		snprintf(connect_name, 100, "127.0.0.1");
		hosting = true;
	}
	std::cout<<"-> "<<connect_name<<std::endl;
	
	std::cout<<"Port [empty=30000]: ";
	char templine[100];
	std::cin.getline(templine, 100);
	unsigned short port;
	if(templine[0] == 0)
		port = 30000;
	else
		port = atoi(templine);

	/*
		Resolution selection
	*/

	u16 screenW = 800;
	u16 screenH = 600;

	/*
	u16 resolutions[][2] = {
		{640,480},
		{800,600},
		{1024,768},
		{1280,1024}
	};

	u16 res_count = sizeof(resolutions)/sizeof(resolutions[0]);
	
	std::cout<<"Select window resolution "
			<<"(type a number and press enter):"<<std::endl;
	for(u16 i=0; i<res_count; i++)
	{
		std::cout<<(i+1)<<": "<<resolutions[i][0]<<"x"
				<<resolutions[i][1]<<std::endl;
	}
	u16 r0;
	std::cin>>r0;
	if(r0 > res_count || r0 == 0)
		r0 = 0;
	u16 screenW = resolutions[r0-1][0];
	u16 screenH = resolutions[r0-1][1];
	*/

	//

	video::E_DRIVER_TYPE driverType;

#ifdef _WIN32
	//driverType = video::EDT_DIRECT3D9; // Doesn't seem to work
	driverType = video::EDT_OPENGL;
#else
	driverType = video::EDT_OPENGL;
#endif

	IrrlichtDevice *device;
	device = createDevice(driverType,
			core::dimension2d<u32>(screenW, screenH),
			16, false, false, false, &receiver);

	if (device == 0)
		return 1; // could not create selected driver.
	
	/*
		Run some speed tests
	*/
	//SpeedTests(device);

	/*
		Continue initialization
	*/

	video::IVideoDriver* driver = device->getVideoDriver();
	// These make the textures not to show at all
	//driver->setTextureCreationFlag(video::ETCF_ALWAYS_16_BIT);
	//driver->setTextureCreationFlag(video::ETCF_OPTIMIZED_FOR_SPEED );

	scene::ISceneManager* smgr = device->getSceneManager();

	gui::IGUIEnvironment* guienv = device->getGUIEnvironment();
	gui::IGUISkin* skin = guienv->getSkin();
	gui::IGUIFont* font = guienv->getFont("../data/fontlucida.png");
	if(font)
		skin->setFont(font);
	//skin->setColor(gui::EGDC_BUTTON_TEXT, video::SColor(255,0,0,0));
	skin->setColor(gui::EGDC_BUTTON_TEXT, video::SColor(255,255,255,255));
	//skin->setColor(gui::EGDC_3D_HIGH_LIGHT, video::SColor(0,0,0,0));
	//skin->setColor(gui::EGDC_3D_SHADOW, video::SColor(0,0,0,0));
	skin->setColor(gui::EGDC_3D_HIGH_LIGHT, video::SColor(255,0,0,0));
	skin->setColor(gui::EGDC_3D_SHADOW, video::SColor(255,0,0,0));
	
	const wchar_t *text = L"Loading...";
	core::vector2d<s32> center(screenW/2, screenH/2);
	core::dimension2d<u32> textd = font->getDimension(text);
	std::cout<<"Text w="<<textd.Width<<" h="<<textd.Height<<std::endl;
	// Have to add a bit to disable the text from word wrapping
	//core::vector2d<s32> textsize(textd.Width+4, textd.Height);
	core::vector2d<s32> textsize(300, textd.Height);
	core::rect<s32> textrect(center - textsize/2, center + textsize/2);

	gui::IGUIStaticText *gui_loadingtext = guienv->addStaticText(
			text, textrect, false, false);
	gui_loadingtext->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT);

	driver->beginScene(true, true, video::SColor(255,0,0,0));
	guienv->drawAll();
	driver->endScene();

	video::SMaterial materials[MATERIALS_COUNT];
	for(u16 i=0; i<MATERIALS_COUNT; i++)
	{
		materials[i].Lighting = false;
		materials[i].BackfaceCulling = false;

		const char *filename = g_material_filenames[i];
		if(filename != NULL){
			video::ITexture *t = driver->getTexture(filename);
			if(t == NULL){
				std::cout<<"Texture could not be loaded: \""
						<<filename<<"\""<<std::endl;
				return 1;
			}
			materials[i].setTexture(0, driver->getTexture(filename));
		}
		//materials[i].setFlag(video::EMF_TEXTURE_WRAP, video::ETC_REPEAT);
		materials[i].setFlag(video::EMF_BILINEAR_FILTER, false);
		//materials[i].setFlag(video::EMF_ANISOTROPIC_FILTER, false);
	}

	// Make a scope here for the client so that it gets removed
	// before the irrlicht device
	{

	std::cout<<"Creating server and client"<<std::endl;
	
	Server *server = NULL;
	if(hosting){
		server = new Server();
		server->start(port);
	}

	Client client(smgr, materials);
	Address connect_address(0,0,0,0, port);
	try{
	connect_address.Resolve(connect_name);
	}
	catch(ResolveError &e)
	{
		std::cout<<"Couldn't resolve address"<<std::endl;
		return 0;
	}
	client.connect(connect_address);

	Player *player = client.getLocalPlayer();

	/*
		Create the camera node
	*/

	scene::ICameraSceneNode* camera = smgr->addCameraSceneNode(
		0, // Camera parent
		v3f(BS*100, BS*2, BS*100), // Look from
		v3f(BS*100+1, BS*2, BS*100), // Look to
		-1 // Camera ID
   	);

	if(camera == NULL)
		return 1;
	
	camera->setFOV(FOV_ANGLE);
	// Just so big a value that everything rendered is visible
	camera->setFarValue(BS*1000);
	
	f32 camera_yaw = 0; // "right/left"
	f32 camera_pitch = 0; // "up/down"
	
	// Random constants
#define WALK_ACCELERATION (4.0 * BS)
#define WALKSPEED_MAX (4.0 * BS)
//#define WALKSPEED_MAX (20.0 * BS)
	f32 walk_acceleration = WALK_ACCELERATION;
	f32 walkspeed_max = WALKSPEED_MAX;
	
	/*
		The mouse cursor needs not be visible, so we hide it via the
		irr::IrrlichtDevice::ICursorControl.
	*/
	device->getCursorControl()->setVisible(false);
	
	gui_loadingtext->remove();
	
	gui::IGUIStaticText *guitext = guienv->addStaticText(
			L"Minetest-c55", core::rect<s32>(5, 5, 5+300, 5+textsize.Y),
			false, false);
	/*
		Main loop
	*/

	bool first_loop_after_window_activation = true;
	
	s32 lastFPS = -1;
	
	// Time is in milliseconds
	u32 lasttime = device->getTimer()->getTime();
	
	while(device->run())
	{
		/*
			Time difference calculation
		*/
		u32 time = device->getTimer()->getTime();
		f32 dtime; // in seconds
		if(time > lasttime)
			dtime = (time - lasttime) / 1000.0;
		else
			dtime = 0;
		lasttime = time;
		
		updateViewingRange(dtime);

		// Collected during the loop and displayed
		core::list< core::aabbox3d<f32> > hilightboxes;
		
		/*
			Special keys
		*/
		if(receiver.IsKeyDown(irr::KEY_ESCAPE))
		{
			break;
		}

		/*
			Player speed control
		*/

		v3f move_direction = v3f(0,0,1);
		move_direction.rotateXZBy(camera_yaw);
		
		v3f speed = v3f(0,0,0);
		if(receiver.IsKeyDown(irr::KEY_KEY_W))
		{
			speed += move_direction;
		}
		if(receiver.IsKeyDown(irr::KEY_KEY_S))
		{
			speed -= move_direction;
		}
		if(receiver.IsKeyDown(irr::KEY_KEY_A))
		{
			speed += move_direction.crossProduct(v3f(0,1,0));
		}
		if(receiver.IsKeyDown(irr::KEY_KEY_D))
		{
			speed += move_direction.crossProduct(v3f(0,-1,0));
		}
		if(receiver.IsKeyDown(irr::KEY_SPACE))
		{
			if(player->touching_ground){
				//player_speed.Y = 30*BS;
				//player.speed.Y = 5*BS;
				player->speed.Y = 6.5*BS;
			}
		}

		// The speed of the player (Y is ignored)
		speed = speed.normalize() * walkspeed_max;
		
		f32 inc = walk_acceleration * BS * dtime;
		
		if(player->speed.X < speed.X - inc)
			player->speed.X += inc;
		else if(player->speed.X > speed.X + inc)
			player->speed.X -= inc;
		else if(player->speed.X < speed.X)
			player->speed.X = speed.X;
		else if(player->speed.X > speed.X)
			player->speed.X = speed.X;

		if(player->speed.Z < speed.Z - inc)
			player->speed.Z += inc;
		else if(player->speed.Z > speed.Z + inc)
			player->speed.Z -= inc;
		else if(player->speed.Z < speed.Z)
			player->speed.Z = speed.Z;
		else if(player->speed.Z > speed.Z)
			player->speed.Z = speed.Z;
		
		/*
			Process environment
		*/
		
		{
			//TimeTaker("client.step(dtime)", device);
			client.step(dtime);
		}

		if(server != NULL){
			//TimeTaker("server->step(dtime)", device);
			server->step(dtime);
		}
		
		/*
			Mouse and camera control
		*/
		
		if(device->isWindowActive())
		{
			if(first_loop_after_window_activation){
				first_loop_after_window_activation = false;
			}
			else{
				s32 dx = device->getCursorControl()->getPosition().X - 320;
				s32 dy = device->getCursorControl()->getPosition().Y - 240;
				camera_yaw -= dx*0.2;
				camera_pitch += dy*0.2;
				if(camera_pitch < -89.9) camera_pitch = -89.9;
				if(camera_pitch > 89.9) camera_pitch = 89.9;
			}
			device->getCursorControl()->setPosition(320, 240);
		}
		else{
			first_loop_after_window_activation = true;
		}
		
		v3f camera_direction = v3f(0,0,1);
		camera_direction.rotateYZBy(camera_pitch);
		camera_direction.rotateXZBy(camera_yaw);

		v3f camera_position =
				player->getPosition() + v3f(0, BS+BS/2, 0);

		camera->setPosition(camera_position);
		camera->setTarget(camera_position + camera_direction);

		if(FIELD_OF_VIEW_TEST){
			//client.m_env.getMap().updateCamera(v3f(0,0,0), v3f(0,0,1));
			client.updateCamera(v3f(0,0,0), v3f(0,0,1));
		}
		else{
			//client.m_env.getMap().updateCamera(camera_position, camera_direction);
			client.updateCamera(camera_position, camera_direction);
		}
		
		/*
			Calculate what block is the crosshair pointing to
		*/
		
		//u32 t1 = device->getTimer()->getTime();
		
		f32 d = 4; // max. distance
		core::line3d<f32> shootline(camera_position,
				camera_position + camera_direction * BS * (d+1));
		
		bool nodefound = false;
		v3s16 nodepos;
		v3s16 neighbourpos;
		core::aabbox3d<f32> nodefacebox;
		f32 mindistance = BS * 1001;
		
		v3s16 pos_i = Map::floatToInt(player->getPosition());

		s16 a = d;
		s16 ystart = pos_i.Y + 0 - (camera_direction.Y<0 ? a : 1);
		s16 zstart = pos_i.Z - (camera_direction.Z<0 ? a : 1);
		s16 xstart = pos_i.X - (camera_direction.X<0 ? a : 1);
		s16 yend = pos_i.Y + 1 + (camera_direction.Y>0 ? a : 1);
		s16 zend = pos_i.Z + (camera_direction.Z>0 ? a : 1);
		s16 xend = pos_i.X + (camera_direction.X>0 ? a : 1);
		
		for(s16 y = ystart; y <= yend; y++){
		for(s16 z = zstart; z <= zend; z++){
		for(s16 x = xstart; x <= xend; x++)
		{
			try{
				//if(client.m_env.getMap().getNode(x,y,z).d == MATERIAL_AIR){
				if(client.getNode(v3s16(x,y,z)).d == MATERIAL_AIR){
					continue;
				}
			}catch(InvalidPositionException &e){
				continue;
			}

			v3s16 np(x,y,z);
			v3f npf = Map::intToFloat(np);
			
			f32 d = 0.01;
			
			v3s16 directions[6] = {
				v3s16(0,0,1), // back
				v3s16(0,1,0), // top
				v3s16(1,0,0), // right
				v3s16(0,0,-1),
				v3s16(0,-1,0),
				v3s16(-1,0,0),
			};

			for(u16 i=0; i<6; i++){
			//{u16 i=3;
				v3f dir_f = v3f(directions[i].X,
						directions[i].Y, directions[i].Z);
				v3f centerpoint = npf + dir_f * BS/2;
				f32 distance =
						(centerpoint - camera_position).getLength();
				
				if(distance < mindistance){
					//std::cout<<"for centerpoint=("<<centerpoint.X<<","<<centerpoint.Y<<","<<centerpoint.Z<<"): distance < mindistance"<<std::endl;
					//std::cout<<"npf=("<<npf.X<<","<<npf.Y<<","<<npf.Z<<")"<<std::endl;
					core::CMatrix4<f32> m;
					m.buildRotateFromTo(v3f(0,0,1), dir_f);

					// This is the back face
					v3f corners[2] = {
						v3f(BS/2, BS/2, BS/2),
						v3f(-BS/2, -BS/2, BS/2+d)
					};
					
					for(u16 j=0; j<2; j++){
						m.rotateVect(corners[j]);
						corners[j] += npf;
						//std::cout<<"box corners["<<j<<"]: ("<<corners[j].X<<","<<corners[j].Y<<","<<corners[j].Z<<")"<<std::endl;
					}

					//core::aabbox3d<f32> facebox(corners[0],corners[1]);
					core::aabbox3d<f32> facebox(corners[0]);
					facebox.addInternalPoint(corners[1]);

					if(facebox.intersectsWithLine(shootline)){
						nodefound = true;
						nodepos = np;
						neighbourpos = np + directions[i];
						mindistance = distance;
						nodefacebox = facebox;
					}
				}
			}
		}}}

		if(nodefound){
			//std::cout<<"nodefound == true"<<std::endl;
			//std::cout<<"nodepos=("<<nodepos.X<<","<<nodepos.Y<<","<<nodepos.Z<<")"<<std::endl;
			//std::cout<<"neighbourpos=("<<neighbourpos.X<<","<<neighbourpos.Y<<","<<neighbourpos.Z<<")"<<std::endl;

			static v3s16 nodepos_old(-1,-1,-1);
			if(nodepos != nodepos_old){
				std::cout<<"Pointing at ("<<nodepos.X<<","
						<<nodepos.Y<<","<<nodepos.Z<<")"<<std::endl;
				nodepos_old = nodepos;

				/*wchar_t positiontext[20];
				swprintf(positiontext, 20, L"(%i,%i,%i)",
						nodepos.X, nodepos.Y, nodepos.Z);
				positiontextgui->setText(positiontext);*/
			}

			hilightboxes.push_back(nodefacebox);
			
			if(receiver.leftclicked){
				std::cout<<"Removing block (MapNode)"<<std::endl;
				u32 time1 = device->getTimer()->getRealTime();

				//client.m_env.getMap().removeNodeAndUpdate(nodepos);
				client.removeNode(nodepos);

				u32 time2 = device->getTimer()->getRealTime();
				u32 dtime = time2 - time1;
				std::cout<<"Took "<<dtime<<"ms"<<std::endl;
			}
			if(receiver.rightclicked){
				std::cout<<"Placing block (MapNode)"<<std::endl;
				u32 time1 = device->getTimer()->getRealTime();

				/*f32 light = client.m_env.getMap().getNode(neighbourpos).light;
				MapNode n;
				n.d = g_selected_material;
				client.m_env.getMap().setNode(neighbourpos, n);
				client.m_env.getMap().nodeAddedUpdate(neighbourpos, light);*/
				MapNode n;
				n.d = g_selected_material;
				client.addNode(neighbourpos, n);
				
				u32 time2 = device->getTimer()->getRealTime();
				u32 dtime = time2 - time1;
				std::cout<<"Took "<<dtime<<"ms"<<std::endl;
			}
		}
		else{
			//std::cout<<"nodefound == false"<<std::endl;
			//positiontextgui->setText(L"");
		}

		receiver.leftclicked = false;
		receiver.rightclicked = false;

		/*
			Update gui stuff
		*/
		
		static u8 old_selected_material = MATERIAL_AIR;
		if(g_selected_material != old_selected_material)
		{
			old_selected_material = g_selected_material;
			wchar_t temptext[50];
			swprintf(temptext, 50, L"Minetest-c55 (F: material=%i)",
					g_selected_material);
			guitext->setText(temptext);
		}
		
		/*
			Drawing begins
		*/

		/*
			Background color is choosen based on whether the player is
			much beyond the initial ground level
		*/
		/*video::SColor bgcolor;
		v3s16 p0 = Map::floatToInt(player->position);
		s16 gy = client.m_env.getMap().getGroundHeight(v2s16(p0.X, p0.Z));
		if(p0.Y > gy - MAP_BLOCKSIZE)
			bgcolor = video::SColor(255,90,140,200);
		else
			bgcolor = video::SColor(255,0,0,0);*/
		video::SColor bgcolor = video::SColor(255,90,140,200);

		driver->beginScene(true, true, bgcolor);

		//std::cout<<"smgr->drawAll()"<<std::endl;

		smgr->drawAll();

		core::vector2d<s32> displaycenter(screenW/2,screenH/2);
		driver->draw2DLine(displaycenter - core::vector2d<s32>(10,0),
				displaycenter + core::vector2d<s32>(10,0),
				video::SColor(255,255,255,255));
		driver->draw2DLine(displaycenter - core::vector2d<s32>(0,10),
				displaycenter + core::vector2d<s32>(0,10),
				video::SColor(255,255,255,255));
		
		video::SMaterial m;
		m.Thickness = 10;
		m.Lighting = false;
		driver->setMaterial(m);

		for(core::list< core::aabbox3d<f32> >::Iterator i=hilightboxes.begin();
				i != hilightboxes.end(); i++){
			driver->draw3DBox(*i, video::SColor(255,0,0,0));
		}

		guienv->drawAll();

		driver->endScene();

		/*
			Drawing ends
		*/

		u16 fps = driver->getFPS();

		if (lastFPS != fps)
		{
			core::stringw str = L"Minetest [";
			str += driver->getName();
			str += "] FPS:";
			str += fps;

			device->setWindowCaption(str.c_str());
			lastFPS = fps;
		}


		/*}
		else
			device->yield();*/
	}

	if(server != NULL)
		delete server;

	} // client is deleted at this point

	/*
	In the end, delete the Irrlicht device.
	*/
	device->drop();
	
	return 0;
}
Ejemplo n.º 3
0
void SpeedTests()
{
	{
		dstream<<"The following test should take around 20ms."<<std::endl;
		TimeTaker timer("Testing std::string speed");
		const u32 jj = 10000;
		for(u32 j=0; j<jj; j++)
		{
			tempstring = "";
			tempstring2 = "";
			const u32 ii = 10;
			for(u32 i=0; i<ii; i++){
				tempstring2 += "asd";
			}
			for(u32 i=0; i<ii+1; i++){
				tempstring += "asd";
				if(tempstring == tempstring2)
					break;
			}
		}
	}
	
	dstream<<"All of the following tests should take around 100ms each."
			<<std::endl;

	{
		TimeTaker timer("Testing floating-point conversion speed");
		tempf = 0.001;
		for(u32 i=0; i<4000000; i++){
			temp16 += tempf;
			tempf += 0.001;
		}
	}
	
	{
		TimeTaker timer("Testing floating-point vector speed");

		tempv3f1 = v3f(1,2,3);
		tempv3f2 = v3f(4,5,6);
		for(u32 i=0; i<10000000; i++){
			tempf += tempv3f1.dotProduct(tempv3f2);
			tempv3f2 += v3f(7,8,9);
		}
	}

	{
		TimeTaker timer("Testing core::map speed");
		
		core::map<v2s16, f32> map1;
		tempf = -324;
		const s16 ii=300;
		for(s16 y=0; y<ii; y++){
			for(s16 x=0; x<ii; x++){
				map1.insert(v2s16(x,y), tempf);
				tempf += 1;
			}
		}
		for(s16 y=ii-1; y>=0; y--){
			for(s16 x=0; x<ii; x++){
				tempf = map1[v2s16(x,y)];
			}
		}
	}

	{
		dstream<<"Around 5000/ms should do well here."<<std::endl;
		TimeTaker timer("Testing mutex speed");
		
		JMutex m;
		m.Init();
		u32 n = 0;
		u32 i = 0;
		do{
			n += 10000;
			for(; i<n; i++){
				m.Lock();
				m.Unlock();
			}
		}
		// Do at least 10ms
		while(timer.getTime() < 10);

		u32 dtime = timer.stop();
		u32 per_ms = n / dtime;
		dstream<<"Done. "<<dtime<<"ms, "
				<<per_ms<<"/ms"<<std::endl;
	}
}
Ejemplo n.º 4
0
void SpeedTests(IrrlichtDevice *device)
{
	/*
		Test stuff
	*/

	//test();
	//return 0;
	/*TestThread thread;
	thread.Start();
	std::cout<<"thread started"<<std::endl;
	while(thread.IsRunning()) sleep(1);
	std::cout<<"thread ended"<<std::endl;
	return 0;*/

	{
		std::cout<<"Testing floating-point conversion speed"<<std::endl;
		u32 time1 = device->getTimer()->getRealTime();
		tempf = 0.001;
		for(u32 i=0; i<10000000; i++){
			temp16 += tempf;
			tempf += 0.001;
		}
		u32 time2 = device->getTimer()->getRealTime();
		u32 fp_conversion_time = time2 - time1;
		std::cout<<"Done. "<<fp_conversion_time<<"ms"<<std::endl;
		//assert(fp_conversion_time < 1000);
	}
	
	{
		std::cout<<"Testing floating-point vector speed"<<std::endl;
		u32 time1 = device->getTimer()->getRealTime();

		tempv3f1 = v3f(1,2,3);
		tempv3f2 = v3f(4,5,6);
		for(u32 i=0; i<40000000; i++){
			tempf += tempv3f1.dotProduct(tempv3f2);
			tempv3f2 += v3f(7,8,9);
		}

		u32 time2 = device->getTimer()->getRealTime();
		u32 dtime = time2 - time1;
		std::cout<<"Done. "<<dtime<<"ms"<<std::endl;
	}

	{
		std::cout<<"Testing core::map speed"<<std::endl;
		u32 time1 = device->getTimer()->getRealTime();
		
		core::map<v2s16, f32> map1;
		tempf = -324;
		for(s16 y=0; y<500; y++){
			for(s16 x=0; x<500; x++){
				map1.insert(v2s16(x,y), tempf);
				tempf += 1;
			}
		}
		for(s16 y=500-1; y>=0; y--){
			for(s16 x=0; x<500; x++){
				tempf = map1[v2s16(x,y)];
			}
		}

		u32 time2 = device->getTimer()->getRealTime();
		u32 dtime = time2 - time1;
		std::cout<<"Done. "<<dtime<<"ms"<<std::endl;
	}

	{
		std::cout<<"Testing mutex speed"<<std::endl;
		u32 time1 = device->getTimer()->getRealTime();
		u32 time2 = time1;
		
		JMutex m;
		m.Init();
		u32 n = 0;
		u32 i = 0;
		do{
			n += 10000;
			for(; i<n; i++){
				m.Lock();
				m.Unlock();
			}
			time2 = device->getTimer()->getRealTime();
		}
		// Do at least 10ms
		while(time2 < time1 + 10);

		u32 dtime = time2 - time1;
		u32 per_ms = n / dtime;
		std::cout<<"Done. "<<dtime<<"ms, "
				<<per_ms<<"/ms"<<std::endl;
	}

	//assert(0);
}