Xwad Posted March 30, 2016 Share Posted March 30, 2016 I added a tank skin for the car alpha. I made a script that a makes possible to rotate the tanks (alpha) turret and the tanks barrel. I have a lot of problems. I hope you can help me. Tanks in advance. 1. The tanks barrel (misc_b) is only rotating up and down if i rotate the camera to the right or to the left:/ It should be go down and up when i look with the camera down and up. 2. The synchronisation is also not working. Only that player can see the tanks rurret rotating who is inside the vehicle. 3. the Roating is too fast. Is it possible to slow it down? CLIENT.LUA function onEnter() local veh = getPedOccupiedVehicle(localPlayer) local id = getElementModel ( veh ) if id == 602 then addEventHandler("onClientRender", getRootElement(), server) end end addEventHandler("onClientVehicleEnter", getRootElement(), onEnter) function server() triggerServerEvent ( "server", resourceRoot) end function turn() local veh = getPedOccupiedVehicle(localPlayer) local id = getElementModel ( veh ) if id == 602 then local vx,vy,vz = getElementRotation(veh) local cx,cy,cz = getPedCameraRotation(localPlayer) local rx,ry,rz = getVehicleComponentRotation( veh, "misc_a" ) setVehicleComponentRotation(veh, "misc_a", rx,ry,0-vz-cx) local rx2,ry2,rz2 = getVehicleComponentRotation( veh, "misc_b" ) setVehicleComponentRotation(veh, "misc_b", 0-cx,ry2,rz2) end end addEvent( "turn", true ) addEventHandler( "turn", resourceRoot, turn ) SERVER.LUA function server () triggerClientEvent ( "turn", getRootElement() ) end addEvent( "server", true ) addEventHandler( "server", resourceRoot, server ) Link to comment
Dealman Posted March 30, 2016 Share Posted March 30, 2016 (edited) I think you'll want to consider re-building that from scratch, that's a rather horrible way of doing things. You trigger a server event with every frame, you then use this server-side event to trigger a client-event on all clients with every frame. Do you even realize how many events you would be triggering like this? While this might potentially work with 1 or 2 players, the lag would be tremendous and you'd be using a lot more bandwidth than necessary. Personally, I'd make use of onClientCursorMove to rotate the turret and barrel instead of fiddling with the camera rotation and component rotations. Basically how games work is they actively re-center the cursor(some games re-center it in the middle of the screen, others at the top left). Also by doing this, you can easily add a variable for mouse sensitivity. If I can find my old script I'll edit this post. This way, you can develop a system similar to this; As for synchronization, I never quite got that far but I would assume using element data would be the way to go. Though, I could be wrong. If I ever get back to working on a gunnery system I'll do some proper performance testing, but I haven't worked with MTA for a while so I'll let someone else chime in on what may be the best way to synchronize it. Edit: I guess scratch that, if you use the method I described above you lose control of your vehicle since you need to have the cursor showing. And as far as I know, there is no possible way to maintain control of your vehicle while also having the cursor showing. It works great for vehicles that are operated by 2 or more people, though. One driver, one gunner. Oh well Edited March 30, 2016 by Guest Link to comment
Xwad Posted March 30, 2016 Author Share Posted March 30, 2016 you mean the gunner passanger script? Link to comment
Dealman Posted March 30, 2016 Share Posted March 30, 2016 you mean the gunner passanger script? Yeah, it's something I worked on a while ago. I just re-visited and made some quick testing. While it works, you can't control your vehicle while the cursor is showing. So you'd probably get the best results the way you're already doing it. Synchronization will be a bit messy, you can synchronize it real fast for testing purposes using element data and onElementDataChange. Though I this is not the best way to do it, obviously. For synchronization you'll want to predict movement to reduce bandwidth and CPU usage as much as possible. Keep in mind that if you get a lot of people playing there will be A LOT of turrets and barrels moving around. Also I edited the previous post, check the bottom of it. Link to comment
Xwad Posted March 30, 2016 Author Share Posted March 30, 2016 check this code. this code makes possible that the onClientCursorMove will also triggered if the cursor is not showing addEventHandler ( "onClientCursorMove", getRootElement ( ), function ( cursorX, cursorY, absoluteX, absoluteY, x2, y2, z2 ) if not isChatBoxInputActive ( ) and not isMainMenuActive ( ) and not isConsoleActive ( ) and not isCursorShowing ( ) then local x1, y1, z1 = getPedBonePosition ( player, 6 ) setCameraMatrix ( x1, y1, z1, x2, y2, z2 ) outputChatBox( "cursor moved") end end ) Link to comment
Xwad Posted March 30, 2016 Author Share Posted March 30, 2016 but actualy the component is not moving:/ function turn() local veh = getPedOccupiedVehicle(localPlayer) local id = getElementModel ( veh ) if id == 602 then outputChatBox( "works") local vx,vy,vz = getElementRotation(veh) local screenx, screeny = getCursorPosition() local rx,ry,rz = getVehicleComponentRotation( veh, "misc_a" ) setVehicleComponentRotation(veh, "misc_a", rx,ry,0-vz-screenx) local rx2,ry2,rz2 = getVehicleComponentRotation( veh, "misc_b" ) setVehicleComponentRotation(veh, "misc_b", 0-screeny,ry2,rz2) end end addEventHandler( "onClientCursorMove", getRootElement, turn) addEventHandler ( "onClientCursorMove", getRootElement ( ), function ( cursorX, cursorY, absoluteX, absoluteY, x2, y2, z2 ) if not isChatBoxInputActive ( ) and not isMainMenuActive ( ) and not isConsoleActive ( ) and not isCursorShowing ( ) then local x1, y1, z1 = getPedBonePosition ( player, 6 ) setCameraMatrix ( x1, y1, z1, x2, y2, z2 ) outputChatBox( "cursor moved") end end ) Link to comment
Einheit-101 Posted March 30, 2016 Share Posted March 30, 2016 triggerServerEvent is one of the best ways to sync this manually and it seems to work for me, although i had no massive battles (yet) with my system. MTA does nothing else with the sync of vehicle dummies and other stuff, send all data once every 50-100ms to the server and he relays it to other clients. Dont even try to predict anything. That elementData thingy does practically the same like triggerServerEvent (Correct me if i`m wrong), you cant magically sync data faster or slower with it. Remember, triggerEvent just triggers an event, its no atomic bomb. However i recommend using a 50ms timer (or 100ms) instead of onClientRender to trigger the Server event / set the Data local update_timer = nil setTimer ( function () update_timer = setTimer ( updatePlayerWeapon, 50, 0 ) end, 500, 1 ) function updatePlayerWeapon () if isElement(TankVehicle) then local _, _, rz = --calculate Z rotation of misc_a local rx, _, _ = --calculate X rotation of misc_b --calculate more stuff setVehicleComponentRotation(TankVehicle, "misc_a", 0, 0, rz) --turret only rotates Z (horizontal) setVehicleComponentRotation(TankVehicle, "misc_b", rx, 0, 0) --barrel only rotates X (up and down) triggerServerEvent ("remoteSyncComponent", TankVehicle, rx, rz) --or setElementData(TankVehicle, "turretRot", {rx, rz}) end end I can't do more, sorry, busy with SAAW, but i plan to do this myself someday in 1-2 months. Link to comment
Xwad Posted March 30, 2016 Author Share Posted March 30, 2016 Just like in the machinegun script:D Thanks EInheit-101! Link to comment
Dealman Posted March 30, 2016 Share Posted March 30, 2016 Doing it every 50ms will result in somewhat choppy rotation though, wouldn't it? Majority of games use prediction for a number of reasons, it's efficient and allows for smooth and interpolated movement. It's a system that would take a lot of time and testing to make - but I'm fairly certain the pros heavily outweigh the cons. Also, triggering events and element data is not the same thing. I believe one of the MTA devs made a nice summary of the differences in some thread a while ago but I can't remember from the top of my head what was said. But keep in mind that MTA already natively use element data for positions and rotations(though I do not know if this is what MTA use to synchronize it). For example, those two produce the same results; local oX, oY, oZ = getElementPosition(objectElement) local oX, oY, oZ = getElementData(objectElement, "posX", "posY", "posZ") I'm not saying one is better than the other, because I quite frankly don't know as I haven't done any testing. It just seems that triggering an event with every frame - or even every 50ms for that matter is not a very good thing to do. Especially not if it goes like this; Player 1 -> Server -> All clients Player 2 -> Server -> All clients Depending on how you set it up, you could end up with a situation where if you have a server with 32 people - one player alone would tell the server to trigger 32 client events. Then, you'd have 31 other clients which tell the server the same thing resulting in 1024 client-events being triggered every ~16ms or 50ms. Does this seem like a logical way to do it for you? Element data is synchronized by default unless you set it to false. So it may be better? I don't know. Link to comment
Einheit-101 Posted March 30, 2016 Share Posted March 30, 2016 @Dealman i guess a MTA dev would have to tell us the best way to do this since no one of us really knows what MTA does under its coat. As for the 50ms laggy rotation: Everything is lagging by default anyway, so does Rhinos turret. I won't try and write a lag compensation to smooth the rotations. Feel free to do it, maybe its really nice? @Xwad: addEventHandler ( "onClientCursorMove", getRootElement ( ), function ( cursorX, cursorY, absoluteX, absoluteY, x2, y2, z2 ) if not isChatBoxInputActive ( ) and not isMainMenuActive ( ) and not isConsoleActive ( ) and not isCursorShowing ( ) then local x1, y1, z1 = getPedBonePosition ( player, 6 ) setCameraMatrix ( x1, y1, z1, x2, y2, z2 ) outputChatBox( "cursor moved") end end ) This code makes onClientCursorMove NOT possible without cursor. onClientCursorMove IS POSSIBLE WITHOUT cursor showing. Its just nobody knows it (i did not know either, thanks for the hint) so that above code snippet is essentially useless in your script. Btw. this is my Mouselook code, idk if it helps someone: function mouseLook(newX, newY) local cam = getCamera() if isElementAttached(cam) and not isCursorShowing() then --make sure the cam is attached to the vehicle local vx = (newX - 0.5)*400 local vy = (newY - 0.5)*400 --reduce this 400 to make the mouseLook slower local x, y, z, rx, ry, rz = getElementAttachedOffsets(cam) setElementAttachedOffsets(cam, x, y, z, rx-vy, ry, rz-vx) end end addEventHandler( "onClientCursorMove", root, mouseLook) Link to comment
Xwad Posted April 3, 2016 Author Share Posted April 3, 2016 so which is the better way to rotate the turret?? onClientCursorMove or setTimer and getPedCameraRotation? Link to comment
Einheit-101 Posted April 9, 2016 Share Posted April 9, 2016 I would use onClientRender + Camera rotation to rotate the turret smoothly and setElementData (rx,ry,rz). Then onElementDataChange for other clients and set component rotation according to the received data. At least that's how I do it with my prototype I can send you some code once it's finished Link to comment
Xwad Posted April 11, 2016 Author Share Posted April 11, 2016 Okay:D Yeah that would be very good, thanks! Link to comment
Einheit-101 Posted April 12, 2016 Share Posted April 12, 2016 (edited) I guess this enables camera turret rotation for custom vehicles as long as the custom vehicle is model 499 or model 455, but you can change that number. My own script here is a lot larger and i extracted the necessary stuff. That means, some things could be missing or so. local syncTimer function onEnter(player, seat) local model = getElementModel(source) if player == localPlayer and (model == 499 or model == 455) and seat == 0 then syncTimer = setTimer(function() setElementData(veh, "turret", getElementData(veh, "turret")) end, 50, 0) addEventHandler("onClientRender", root, moveLocalTurret) end end addEventHandler("onClientVehicleEnter", root, onEnter) function moveLocalTurret() if not isPedInVehicle(localPlayer) then removeEventHandler("onClientRender", root, moveLocalTurret) if syncTimer ~= nil then killTimer(syncTimer) syncTimer = nil end else local veh = getPedOccupiedVehicle(localPlayer) local model = getElementModel(veh) local cam = getCamera() if isElementStreamedIn(veh) and (model == 499 or model == 455) and getVehicleController(veh) == localPlayer and not isPedDoingTask(localPlayer, "TASK_SIMPLE_CAR_GET_OUT") and not isElementAttached(cam) and not getKeyState("c") then local rotSpeed = 2 local rx, ry, rz = getElementRotation(veh) local rotV, _, rotH = getCameraRotation() rotH = (-1*rotH)-rz local _, _, rh = getVehicleComponentRotation(veh, "misc_a") local ry, _, _ = getVehicleComponentRotation(veh, "misc_b") rotV = math.rad(rotV) local angleDiff = rh-rotH if math.abs(angleDiff) > 180 then if rh > rotH then angleDiff = -1*((360-rh)+rotH) else angleDiff = (360-rotH)+rh end end local movement = 0 if angleDiff > 0.05 and angleDiff <= 0.2 then movement = -0.025 elseif angleDiff > 0.2 and angleDiff <= 2 then movement = -0.1 elseif angleDiff > 2 then movement = -1 elseif angleDiff < -0.05 and angleDiff >= -0.2 then movement = 0.025 elseif angleDiff < -0.2 and angleDiff >= -2 then movement = 0.1 elseif angleDiff < -2 then movement = 1 end newRZ = rh + (movement*rotSpeed) if rotV > 1 then rotV = rotV-(2*math.pi) end movement = 0 local verticalDiff = math.rad(ry) if verticalDiff > 3 then verticalDiff = verticalDiff-(2*math.pi) end verticalDiff = verticalDiff-(rotV+0.14) if verticalDiff > 0.001 and verticalDiff <= 0.01 then movement = -0.025 elseif verticalDiff > 0.01 then movement = -0.25 elseif verticalDiff < -0.001 and verticalDiff >= -0.01 then movement = 0.025 elseif verticalDiff < -0.01 then movement = 0.25 end local newRY = ry+(movement*rotSpeed) if newRY ~= ry or newRZ ~= rh then if (newRY < 350 and newRY > 150) or (newRY > 24 and newRY < 150) then newRY = ry end setVehicleComponentRotation(veh, "misc_a", 0, 0, newRZ) setVehicleComponentRotation(veh, "misc_b", newRY, 0, 0) setElementData(veh, "turret", {newRZ, newRY}, false) end end end end function turretRotator(dataName) if dataName == "turret" and getVehicleController(source) ~= localPlayer then setVehicleComponentRotation(source, "misc_a", 0, 0, getElementData(source, "turret")[1]) setVehicleComponentRotation(source, "misc_b", getElementData(source, "turret")[2], 0, 0) end end addEventHandler ( "onClientElementDataChange", root, turretRotator) function getCameraRotation() local px, py, pz, lx, ly, lz = getCameraMatrix() local rotz = 6.2831853071796 - math.atan2 ( ( lx - px ), ( ly - py ) )% 6.2831853071796 local rotx = math.atan2 ( lz - pz, getDistanceBetweenPoints2D ( lx, ly,px, py ) ) rotx = math.deg(rotx) rotz = -math.deg(rotz) return rotx, 180, rotz end Edited April 12, 2016 by Guest Link to comment
Xwad Posted April 12, 2016 Author Share Posted April 12, 2016 Really thanks Einheit-101! Link to comment
Saml1er Posted April 12, 2016 Share Posted April 12, 2016 Element data is a bad solution in this case. Do it like this: create a colshape in server side for every individual player and attach them to players, when a player is shooting/rotation with a weapon then simply get the current players in the colshape and trigger. This will reduce a huge amount of traffic. Also when you trigger use triggerClientEvent then pass a table of player elements that you want to sent data to instead of looping through table and triggering for each client ( It will help a lot if you don't belive me then try sending 3 mb of data using triggerLatentClientEvent using a loop for 20 players and see what happens ) Link to comment
Einheit-101 Posted April 12, 2016 Share Posted April 12, 2016 This wont reduce a lot since most players are in the streaming radius of the tanks anyway in our gamemodes. And for, lets say, maybe 10 or max. 20 tanks the bandwith used by element data is not much if you pass 2 numbers in a table every 50ms. Also, sending of element data is only done if the turret really rotates (see line 66). If the player is not moving the turret, no element data is sent. The next thing i research is if MTA is syncing only the last elementData within the 50ms span or if it transfers all element datas that have been calculated within the 50ms... €DIT2::: yes, MTA sends 4x the same elementData every 50ms when its calculated 4x withing 50ms, so i will have to add a separate timer that sends the elementData only once every 50ms. Example Video: €DIT::: I forgot to add the getCameraRotation part. Add this: function getCameraRotation() local px, py, pz, lx, ly, lz = getCameraMatrix() local rotz = 6.2831853071796 - math.atan2 ( ( lx - px ), ( ly - py ) )% 6.2831853071796 local rotx = math.atan2 ( lz - pz, getDistanceBetweenPoints2D ( lx, ly,px, py ) ) rotx = math.deg(rotx) rotz = -math.deg(rotz) return rotx, 180, rotz end €DIT3::: i changed the elementData syncing so it sends only the last calcualted elementData every 50ms: change setElementData(veh, "turret", {newRZ, newRY}) to setElementData(veh, "turret", {newRZ, newRY}, false) And add this to the top of the script to the global vars local syncTimer replace the onEnter function: function onEnter(player, seat) local model = getElementModel(source) if player == localPlayer and (model == 499 or model == 455) and seat == 0 then syncTimer = setTimer(function() setElementData(veh, "turret", getElementData(veh, "turret")) end, 50, 0) addEventHandler("onClientRender", root, moveLocalTurret) end end addEventHandler("onClientVehicleEnter", root, onEnter) replace the top of the moveLocalTurret function: function moveLocalTurret() if not isPedInVehicle(localPlayer) then removeEventHandler("onClientRender", root, moveLocalTurret) if syncTimer ~= nil then killTimer(syncTimer) syncTimer = nil end else It should work €DIT4::: edited last post on page 1, everything is in there, just copy it Link to comment
Saml1er Posted April 12, 2016 Share Posted April 12, 2016 Syncing will always be worse if you're not using UDP. I know most programmers will not agree to this because its too much technical work for a small gamemode but thats the only efficient way. Basically triggerClientEvent/triggerServerEvent uses TCP so that no packet is lost but its very slow as compared to UDP and it will usually lagg if it is used for syncing. Newbies usually use setElementData for FPS in onClientRender but these poor souls never realise that its a complete disaster when you have 100+ players in your server. However, if you don't increase the maximum player limit then your script should do fine. ^^ Link to comment
Einheit-101 Posted April 12, 2016 Share Posted April 12, 2016 Maximum Player limit is 52. How can we use UDP? Link to comment
Saml1er Posted April 18, 2016 Share Posted April 18, 2016 Sorry for late reply.. Maximum Player limit is 52.How can we use UDP? Well you will need to do that in c++. You can use boost serialization for datagrams ( packets ) and then send them over the network and unpack them at the server side but the easy way would be to use RakNet as RakNet is open source since 2015 so you take full advantage of it to send/receive packets as it is the most easy way to do so. Also yeah you will need to use a module for server side as you will need to unpack the packets. I'll quote myself: Well if you are ready to go through trouble then you can write a simple UDP server and client script to send and receive data in C or C++. I guess you can compile it (DLL) and use it in MTA for your server. I wrote a very simple working script for my test multiplayer game. Server side code for linux ( Since most servers are running on linux ) in C: #include <stdio.h> #include <time.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #define PORT 28009 // Choose a free port #define MAX_CLIENTS 250 // sync between clients int compareAddress ( struct sockaddr_in *a1, struct sockaddr_in *a2) // useful function { int Match = 0; if ( sizeof (a1) == sizeof ( a2 ) && (a1->sin_family == a2->sin_family) && (a1->sin_addr.s_addr == a2->sin_addr.s_addr) && (a1->sin_port == a2->sin_port) ) { Match++; } return Match; } // for now we will not use this function void sendQuitMessagetoAll (int listener, struct sockaddr_in cli_addr[], struct sockaddr_in *cli_source ) { char niceArray [] = "One of the player has quit the game."; int clients = sizeof ( cli_addr ); int i; for ( i=0; i<clients; i++){ int len = sizeof(niceArray); int total = 0; // how many bytes we've sent int bytesleft = len; // how many we have left to send int n; while(total < len) { n = sendto(listener, niceArray+total, bytesleft, 0, (struct sockaddr*)&cli_addr[i], sizeof (cli_addr[i]) ); if (n == -1) { break; } total += n; bytesleft -= n; } len = total; // return number actually sent here } } int main(void) { fd_set master; // master file descriptor list int clients, client_addrSize[MAX_CLIENTS], listener, retval, nbytes, i, c, j, newLoopPos, bytesSent; int len, total, bytesleft, n, clientRemoved; clients = 0; // 0 connected clients socklen_t slen_temp; time_t last_update[MAX_CLIENTS]; // checking disconnection or time out double diff_t; struct sockaddr_in myaddr, cli_addr[MAX_CLIENTS], cli_temp; ; /* our address and client address*/ socklen_t addrlen = sizeof(myaddr); /* length of addresses */ memset((char *)&myaddr, 0, sizeof(myaddr)); myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = htonl(INADDR_ANY); myaddr.sin_port = htons(PORT); /* create a UDP socket */ if ((listener = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("cannot create socket\n"); return 0; } /* bind the socket to any valid IP address and a specific port */ if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) { perror("bind failed"); return 0; } // main loop for( ; ; ) { FD_ZERO(&master); FD_SET(listener, &master); retval = select(listener+1, &master, NULL, NULL, NULL) ; if (retval == -1) { perror("select"); exit(4); } else if ( retval ) { printf (" data available\n "); // run through the existing connections looking for data to read if (FD_ISSET(listener, &master)) { // we got one!! char buf[1024]; // buffer for client data // handle data from a client if ((n = recvfrom(listener, buf, sizeof(buf), 0, (struct sockaddr*)&cli_temp, &slen_temp)) <= 0) { // got error or connection closed by client if (nbytes == 0) // connection closed printf("selectserver: socket %d hung up\n", listener); else perror("ERROR: recvfrom"); } else { char niceArray[n]; for(i = 0; i < n; i++) niceArray[i] = buf[i]; j = 0; // j wil act as bool for checking if client address is already stored in array or no int this_length = sizeof ( (struct sockaddr*)&cli_temp); if (clients < MAX_CLIENTS) { for(i = 0; i < clients; i++) { // struct sockaddr*)&cli_addr[i] if ( compareAddress(&cli_addr[i], &cli_temp) > 0 ) { j++; } } } if ( j == 0 ) { // so yeah client doesn't exist so we store the address and last update time cli_addr[clients] = cli_temp; last_update[clients] = time(NULL); clients++; printf("\n Client Added. \n"); } newLoopPos = 0; // start loop from 0 do { clientRemoved = 0; for(i = newLoopPos; i < clients; i++) { diff_t = difftime( time(NULL), last_update[i] ); if ( diff_t >= 15 ) { // client has time out or disconnect if ( (i+1) == clients) { // we don't need to DO the do-while loop AGAIN if we are removing the last element // nothing here <!-- s;) --><img src=\"{SMILIES_PATH}/icon_wink.gif\" alt=\";)\" title=\"Wink\" /><!-- s;) --> } else clientRemoved++; newLoopPos = i; clients--; struct sockaddr_in cli_source; cli_source = cli_addr[i]; // client that got disconnected for ( c = i - 1 ; c < MAX_CLIENTS - 1 ; c++ ) { // then its better to remove him from array cli_addr[c] = cli_addr[c+1]; last_update[c] = last_update[c+1]; } // we surely don't want to display disconnection messages from past 10 hours or so <!-- s;) --><img src=\"{SMILIES_PATH}/icon_wink.gif\" alt=\";)\" title=\"Wink\" /><!-- s;) --> if ( diff_t < 60 ) { /* //lets disable quit message for a while until we add support in client side for reading sendQuitMessagetoAll ( listener, cli_addr, &cli_source ); */ } break; // it's important to break for loop now since we don't need to loop again wee } /* DEBUG PURPOSE Link to comment
Einheit-101 Posted April 18, 2016 Share Posted April 18, 2016 Unfortunately I am not familiar with C++. But the improved sync I wrote with triggerServerEvent once every 50ms works pretty well, the turrets are rotating with ~20 fps and all players are fine with it. Rhino itself is Not better. Link to comment
Xwad Posted April 18, 2016 Author Share Posted April 18, 2016 Einheit one short question: whats the name of the dodos wheels compoent? misc_what? Just like on your server, when a player enters the dodo and prress g to move the gear Link to comment
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now