Complete disaster-recovery snapshot: engine/game source, game data assets, VC6 toolchain + DX SDKs, build outputs, deployed game, and _UNUSED archive. Large binaries in Git LFS; text preserved byte-for-byte (core.autocrlf=false, no eol attributes). See RECOVERY.md for the one-clone rebuild procedure.
659 lines
16 KiB
C++
659 lines
16 KiB
C++
#include "AdeptHeaders.hpp"
|
|
#include "GameSpy.h"
|
|
#define MAX_SERVERS_PER_MACHINE 100
|
|
|
|
#ifndef NEW_GAMESPY
|
|
/******
|
|
goautil.c
|
|
GameSpy Developer SDK
|
|
|
|
Copyright 1999 GameSpy Industries, Inc
|
|
|
|
Suite E-204
|
|
2900 Bristol Street
|
|
Costa Mesa, CA 92626
|
|
(714)549-7689
|
|
Fax(714)549-0757
|
|
|
|
******
|
|
|
|
Please see the GameSpy Developer SDK documentation for more
|
|
information
|
|
|
|
Updated 6/9/99 - DDW
|
|
Added get_sockaddrin function, and use for resolving
|
|
Made portable with additions from CEngine code
|
|
Double check that we don't bind to localhost (instead use INADDR_ANY)
|
|
|
|
Updated 9/1/99 - DDW
|
|
Add the ability to pass in 0 as the queryport, to allocate it
|
|
automatically.
|
|
|
|
Updated 10/11/99 - DDW
|
|
TCP Heartbeat support (#define TCP_HEARTBEATS)
|
|
|
|
Updated 11/20/99 - BGW
|
|
Split goa_process_queries into two functions: goa_check_queries() and
|
|
goa_check_send_heartbeat().
|
|
|
|
Updated 3/19/00 - DDW
|
|
Added Dreamcast CE Support
|
|
|
|
******/
|
|
|
|
/********
|
|
INCLUDES
|
|
********/
|
|
#include "nonport.h"
|
|
#include "goautil.h"
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
|
|
#include <string.h>
|
|
#ifndef UNDER_CE
|
|
#include <assert.h>
|
|
#else
|
|
#define assert(a)
|
|
#endif
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/********
|
|
DEFINES
|
|
********/
|
|
#define HB_TIME 300000 /* 5 minutes */
|
|
#define MAX_DATA_SIZE 1400
|
|
#define INBUF_LEN 256
|
|
#define BUF_SIZE 1400
|
|
|
|
/*****
|
|
TCP_HEARTBEATS
|
|
Define this to use reliable heartbeats. Only needed for certain
|
|
classes of peer-to-peer games, please contact us if you are unsure
|
|
whether you need this or not.
|
|
******/
|
|
//#define TCP_HEARTBEATS
|
|
|
|
/********
|
|
GLOBAL VARS
|
|
********/
|
|
goa_querycallback_t goa_basic_callback = NULL;
|
|
goa_querycallback_t goa_info_callback = NULL;
|
|
goa_querycallback_t goa_rules_callback = NULL;
|
|
goa_querycallback_t goa_players_callback = NULL;
|
|
char goa_secret_key[256];
|
|
|
|
/********
|
|
TYPEDEFS
|
|
********/
|
|
typedef unsigned char uchar;
|
|
|
|
typedef enum {qtunknown, qtbasic, qtinfo, qtrules, qtplayers, qtstatus, qtpackets, qtecho, qtsecure} query_t;
|
|
|
|
|
|
/********
|
|
VARS
|
|
********/
|
|
static const char *queries[]={"","basic","info","rules","players","status","packets", "echo", "secure"};
|
|
|
|
static unsigned long lastheartbeat = 0;
|
|
static int queryid = 1;
|
|
static int packetnumber;
|
|
static int qport;
|
|
static void *udata = NULL;
|
|
static char gname[24];
|
|
|
|
static SOCKET querysock = INVALID_SOCKET;
|
|
static SOCKET hbsock = INVALID_SOCKET;
|
|
|
|
|
|
/********
|
|
PROTOTYPES
|
|
********/
|
|
static void send_heartbeat(int statechanged);
|
|
static void parse_query(char *query, struct sockaddr *sender);
|
|
static int get_sockaddrin(char *host, int port, struct sockaddr_in *saddr, struct hostent **savehent);
|
|
static int do_connect(SOCKET sock, char *addr, int port);
|
|
|
|
|
|
/****************************************************************************/
|
|
/* PUBLIC FUNCTIONS */
|
|
/****************************************************************************/
|
|
|
|
/* goa_init: Initializes the sockets, etc. Returns an error value
|
|
if an error occured, or 0 otherwise */
|
|
int goa_init(int queryport, char *gamename, char *ip, void *userdata)
|
|
{
|
|
int lasterror;
|
|
struct sockaddr_in saddr;
|
|
int saddrlen;
|
|
|
|
//save off query port,gamename and userdata
|
|
qport = queryport;
|
|
strncpy(gname, gamename, 23);
|
|
udata = userdata;
|
|
|
|
//create our sockets
|
|
SocketStartUp();
|
|
|
|
do
|
|
{
|
|
querysock = socket(PF_INET, SOCK_DGRAM, 0);
|
|
#ifdef TCP_HEARTBEATS
|
|
hbsock = socket(PF_INET, SOCK_STREAM, 0);
|
|
#else
|
|
hbsock = socket(PF_INET, SOCK_DGRAM, 0);
|
|
#endif
|
|
if (INVALID_SOCKET == querysock || INVALID_SOCKET == hbsock)
|
|
{
|
|
return E_GOA_WSOCKERROR;
|
|
}
|
|
|
|
get_sockaddrin(ip,qport,&saddr,NULL);
|
|
if (saddr.sin_addr.s_addr == htonl(0x7F000001)) //localhost -- we don't want that!
|
|
saddr.sin_addr.s_addr = INADDR_ANY;
|
|
|
|
lasterror = bind(querysock, (struct sockaddr *)&saddr, sizeof(saddr));
|
|
if (lasterror == 0)
|
|
break;
|
|
|
|
//shutdown(querysock, SO_BOTH);
|
|
//shutdown(hbsock, SO_BOTH);
|
|
closesocket(querysock);
|
|
closesocket(hbsock);
|
|
|
|
qport++;
|
|
} while (qport < (queryport + MAX_SERVERS_PER_MACHINE));
|
|
|
|
if (lasterror)
|
|
return E_GOA_BINDERROR;
|
|
|
|
|
|
if (qport == 0) //we need to allocate it ourselves
|
|
{
|
|
saddrlen = sizeof(saddr);
|
|
lasterror = getsockname(querysock,(struct sockaddr *)&saddr, &saddrlen);
|
|
if (lasterror)
|
|
return E_GOA_BINDERROR;
|
|
qport = ntohs(saddr.sin_port);
|
|
}
|
|
|
|
int nGameSpyPort;
|
|
GameSpyOld::GetGameSpyAddress(GameSpyOld::szGameSpyAddress, 256);
|
|
nGameSpyPort = GameSpyOld::GetHeartbeatPort();
|
|
|
|
return do_connect(hbsock, GameSpyOld::szGameSpyAddress, nGameSpyPort);
|
|
|
|
}
|
|
|
|
/* goa_process_queries: Processes any waiting queries, and sends a
|
|
heartbeat if 5 minutes have elapsed */
|
|
void goa_process_queries(void)
|
|
{
|
|
goa_check_send_heartbeat();
|
|
goa_check_queries();
|
|
}
|
|
|
|
/* check_queries: Processes any waiting queries */
|
|
void goa_check_queries(void)
|
|
{
|
|
static char indata[INBUF_LEN]; //256 byte input buffer
|
|
struct sockaddr saddr;
|
|
int saddrlen = sizeof(struct sockaddr);
|
|
fd_set set;
|
|
struct timeval timeout = {0,0};
|
|
int error;
|
|
|
|
if (INVALID_SOCKET == querysock)
|
|
return; //no sockets to work with!
|
|
|
|
FD_ZERO ( &set );
|
|
FD_SET ( querysock, &set );
|
|
|
|
while (true)
|
|
{
|
|
error = select(FD_SETSIZE, &set, NULL, NULL, &timeout);
|
|
if (SOCKET_ERROR == error || 0 == error)
|
|
return;
|
|
//else we have data
|
|
error = recvfrom(querysock, indata, INBUF_LEN - 1, 0, &saddr, &saddrlen);
|
|
if (error != SOCKET_ERROR)
|
|
{
|
|
indata[error] = '\0';
|
|
parse_query(indata, &saddr);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* check_send_heartbeat: Perform any scheduled outgoing
|
|
heartbeats, (every 5 minutes) */
|
|
void goa_check_send_heartbeat(void)
|
|
{
|
|
unsigned int tc = current_time();
|
|
|
|
if (INVALID_SOCKET == hbsock)
|
|
return; //no sockets to work with!
|
|
|
|
//check if we need to send a heartbet
|
|
if (tc - lastheartbeat > HB_TIME)
|
|
send_heartbeat(0);
|
|
}
|
|
|
|
/* goa_send_statechanged: Sends a statechanged heartbeat, call when
|
|
your gamemode changes */
|
|
void goa_send_statechanged(void)
|
|
{
|
|
send_heartbeat(1);
|
|
}
|
|
|
|
/* goa_shutdown: Cleans up the sockets and shuts down */
|
|
void goa_shutdown(void)
|
|
{
|
|
if (INVALID_SOCKET != querysock)
|
|
{
|
|
closesocket(querysock);
|
|
querysock = INVALID_SOCKET;
|
|
}
|
|
|
|
if (INVALID_SOCKET != hbsock)
|
|
{
|
|
closesocket(hbsock);
|
|
hbsock = INVALID_SOCKET;
|
|
}
|
|
lastheartbeat = 0;
|
|
SocketShutDown();
|
|
}
|
|
|
|
/****************************************************************************/
|
|
|
|
static int do_connect(SOCKET sock, char *addr, int port)
|
|
{
|
|
struct sockaddr_in saddr;
|
|
|
|
if (!get_sockaddrin(addr, port,&saddr,NULL))
|
|
return E_GOA_CONNERROR;
|
|
|
|
if (connect (sock, (struct sockaddr *) &saddr, sizeof(saddr)) == SOCKET_ERROR)
|
|
{
|
|
return E_GOA_CONNERROR;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Return a sockaddrin for the given host (numeric or DNS) and port)
|
|
Returns the hostent in savehent if it is not NULL */
|
|
static int get_sockaddrin(char *host, int port, struct sockaddr_in *saddr, struct hostent **savehent)
|
|
{
|
|
struct hostent *hent=NULL;
|
|
|
|
saddr->sin_family = AF_INET;
|
|
saddr->sin_port = htons((unsigned short)port);
|
|
if (host == NULL)
|
|
saddr->sin_addr.s_addr = INADDR_ANY;
|
|
else
|
|
saddr->sin_addr.s_addr = inet_addr(host);
|
|
|
|
if (saddr->sin_addr.s_addr == INADDR_NONE && strcmp(host,"255.255.255.255") != 0)
|
|
{
|
|
hent = gethostbyname(host);
|
|
if (!hent)
|
|
return 0;
|
|
saddr->sin_addr.s_addr = *(u_long *)hent->h_addr_list[0];
|
|
}
|
|
if (savehent != NULL)
|
|
*savehent = hent;
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* value_for_key: this returns a value for a certain key in s, where s is a string
|
|
containing key\value pairs. If the key does not exist, it returns NULL.
|
|
Note: the value is stored in a common buffer. If you want to keep it, make a copy! */
|
|
static char *value_for_key(const char *s, const char *key)
|
|
{
|
|
static int valueindex;
|
|
char *pos,*pos2;
|
|
char keyspec[256]="\\";
|
|
static char value[2][256];
|
|
|
|
valueindex ^= 1;
|
|
strcat(keyspec,key);
|
|
strcat(keyspec,"\\");
|
|
pos = strstr(s,keyspec);
|
|
if (!pos)
|
|
return NULL;
|
|
pos += strlen(keyspec);
|
|
pos2 = value[valueindex];
|
|
while (*pos && *pos != '\\')
|
|
*pos2++ = *pos++;
|
|
*pos2 = '\0';
|
|
return value[valueindex];
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Various encryption / encoding routines */
|
|
|
|
#ifndef _GUTIL
|
|
#define _GUTIL
|
|
static void swap_byte ( uchar *a, uchar *b )
|
|
{
|
|
uchar swapByte;
|
|
|
|
swapByte = *a;
|
|
*a = *b;
|
|
*b = swapByte;
|
|
}
|
|
|
|
static uchar encode_ct ( uchar c )
|
|
{
|
|
if (c < 26) return (uchar)('A'+c);
|
|
if (c < 52) return (uchar)('a'+c-26);
|
|
if (c < 62) return (uchar)('0'+c-52);
|
|
if (c == 62) return (uchar)('+');
|
|
if (c == 63) return (uchar)('/');
|
|
|
|
return 0;
|
|
}
|
|
|
|
void gs_encode ( uchar *ins, int size, uchar *result )
|
|
{
|
|
int i,pos;
|
|
uchar trip[3];
|
|
uchar kwart[4];
|
|
|
|
i=0;
|
|
while (i < size)
|
|
{
|
|
for (pos=0 ; pos <= 2 ; pos++, i++)
|
|
if (i < size) trip[pos] = *ins++;
|
|
else trip[pos] = '\0';
|
|
kwart[0] = (uchar)( (trip[0]) >> 2);
|
|
kwart[1] = (uchar)((((trip[0]) & 3) << 4) + ((trip[1]) >> 4));
|
|
kwart[2] = (uchar)((((trip[1]) & 15) << 2) + ((trip[2]) >> 6));
|
|
kwart[3] = (uchar)( (trip[2]) & 63);
|
|
for (pos=0; pos <= 3; pos++) *result++ = encode_ct(kwart[pos]);
|
|
}
|
|
*result='\0';
|
|
}
|
|
|
|
void gs_encrypt ( uchar *key, int key_len, uchar *buffer_ptr, int buffer_len )
|
|
{
|
|
short counter;
|
|
uchar x, y, xorIndex;
|
|
uchar state[256];
|
|
|
|
for ( counter = 0; counter < 256; counter++) state[counter] = (uchar) counter;
|
|
|
|
x = 0; y = 0;
|
|
for ( counter = 0; counter < 256; counter++)
|
|
{
|
|
y = (uchar)((key[x] + state[counter] + y) % 256);
|
|
x = (uchar)((x + 1) % key_len);
|
|
swap_byte ( &state[counter], &state[y] );
|
|
}
|
|
|
|
x = 0; y = 0;
|
|
for ( counter = 0; counter < buffer_len; counter ++)
|
|
{
|
|
x = (uchar)((x + buffer_ptr[counter] + 1) % 256);
|
|
y = (uchar)((state[x] + y) % 256);
|
|
swap_byte ( &state[x], &state[y] );
|
|
xorIndex = (uchar)((state[x] + state[y]) % 256);
|
|
buffer_ptr[counter] ^= state[xorIndex];
|
|
}
|
|
}
|
|
/*****************************************************************************/
|
|
#endif
|
|
|
|
/* packet_send: sends a key\value packet. Appends the queryid
|
|
key\value pair. Clears the buffer */
|
|
static void packet_send(struct sockaddr *addr, char *buffer)
|
|
{
|
|
char keyvalue[80];
|
|
|
|
if (strlen(buffer) == 0)
|
|
return; //dont need to send an empty one!
|
|
packetnumber++; //packet numbers start at 1
|
|
sprintf(keyvalue,"\\queryid\\%d.%d",queryid, packetnumber);
|
|
strcat(buffer,keyvalue);
|
|
#ifdef _DEBUG
|
|
OutputDebugString("\n");
|
|
OutputDebugString(buffer);
|
|
OutputDebugString("\n");
|
|
#endif
|
|
sendto(querysock, buffer, strlen(buffer), 0, addr, sizeof(struct sockaddr));
|
|
buffer[0]='\0';
|
|
}
|
|
|
|
/* buffer_send: appends buffer with newdata. If the combined
|
|
size would be too large, it flushes buffer first. Space is reserved
|
|
on the total size to allow for the queryid key\value */
|
|
static void buffer_send(struct sockaddr *sender, char *buffer, char *newdata)
|
|
{
|
|
char *pos, *lastkey;
|
|
int bcount = 0;
|
|
|
|
if (strlen(buffer) + strlen(newdata) < MAX_DATA_SIZE - 50)
|
|
{
|
|
strcat(buffer, newdata);
|
|
} else
|
|
{//test this!
|
|
if (strlen(newdata) > MAX_DATA_SIZE - 50) //incoming data is too big already!
|
|
{
|
|
lastkey = pos = newdata;
|
|
while (pos - newdata < MAX_DATA_SIZE-50)
|
|
{
|
|
if ('\\' == *pos)
|
|
{
|
|
if (bcount % 2 == 0)
|
|
lastkey = pos;
|
|
bcount++;
|
|
}
|
|
pos++;
|
|
}
|
|
if (lastkey == newdata)
|
|
return; //endless loop - single key was too big!
|
|
*lastkey = '\0';
|
|
buffer_send(sender, buffer, newdata);
|
|
*lastkey = '\\';
|
|
buffer_send(sender, buffer, lastkey); //send the rest!
|
|
} else
|
|
{
|
|
packet_send(sender, buffer);
|
|
strcpy(buffer,newdata);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/* send_basic: sends a response to the basic query */
|
|
static void send_basic(struct sockaddr *sender, char *outbuf)
|
|
{
|
|
char keyvalue[BUF_SIZE] = "";
|
|
|
|
assert(goa_basic_callback);
|
|
goa_basic_callback(keyvalue, sizeof(keyvalue), udata);
|
|
|
|
buffer_send(sender, outbuf, keyvalue);
|
|
|
|
|
|
}
|
|
|
|
/* send_info: sends a response to the info query */
|
|
static void send_info(struct sockaddr *sender, char *outbuf)
|
|
{
|
|
char keyvalue[BUF_SIZE] = "";
|
|
|
|
assert(goa_info_callback);
|
|
goa_info_callback(keyvalue, sizeof(keyvalue), udata);
|
|
buffer_send(sender, outbuf, keyvalue);
|
|
|
|
}
|
|
|
|
/* send_rules: sends a response to the rules query. */
|
|
static void send_rules(struct sockaddr *sender, char *outbuf)
|
|
{
|
|
char keyvalue[BUF_SIZE] = "";
|
|
|
|
assert(goa_rules_callback);
|
|
goa_rules_callback(keyvalue, sizeof(keyvalue), udata);
|
|
buffer_send(sender, outbuf, keyvalue);
|
|
|
|
}
|
|
|
|
/* send_players: sends the players and their information.*/
|
|
static void send_players(struct sockaddr *sender, char *outbuf)
|
|
{
|
|
char keyvalue[BUF_SIZE] = "";
|
|
|
|
assert(goa_players_callback);
|
|
goa_players_callback(keyvalue, sizeof(keyvalue), udata);
|
|
buffer_send(sender, outbuf, keyvalue);
|
|
|
|
}
|
|
|
|
/* send_echo: bounces the echostr back to sender
|
|
Note: you should always make sure that your echostr doesn't exceed the MAX_DATA_SIZE*/
|
|
static void send_echo(struct sockaddr *sender, char *outbuf,char *echostr)
|
|
{
|
|
char keyvalue[MAX_DATA_SIZE];
|
|
|
|
if (strlen(echostr) > MAX_DATA_SIZE - 50)
|
|
return;
|
|
sprintf(keyvalue,"\\echo\\%s",echostr);
|
|
buffer_send(sender, outbuf, keyvalue);
|
|
|
|
}
|
|
|
|
/* send_final: sends the remaining data in outbuf. Appends the final
|
|
key\value to the end. Also adds validation if required. */
|
|
static void send_final(struct sockaddr *sender, char *outbuf,char *validation)
|
|
{
|
|
char keyvalue[256];
|
|
char encrypted_val[128]; //don't need to null terminate
|
|
char encoded_val[200];
|
|
int keylen;
|
|
|
|
if (validation[0])
|
|
{
|
|
keylen = strlen(validation);
|
|
if (keylen > 128) return;
|
|
strcpy(encrypted_val, validation);
|
|
gs_encrypt((uchar *)goa_secret_key, strlen(goa_secret_key), (uchar *)encrypted_val, keylen);
|
|
gs_encode((uchar *)encrypted_val,keylen, (uchar *)encoded_val);
|
|
sprintf(keyvalue,"\\validate\\%s",encoded_val);
|
|
buffer_send(sender, outbuf, keyvalue);
|
|
}
|
|
|
|
sprintf(keyvalue,"\\final\\");
|
|
buffer_send(sender, outbuf, keyvalue);
|
|
packet_send(sender, outbuf);
|
|
}
|
|
|
|
|
|
/* parse_query: parse an incoming query (which may contain 1 or more
|
|
individual queries) and reply to each query */
|
|
static void parse_query(char *query, struct sockaddr *sender)
|
|
{
|
|
query_t querytype;
|
|
char buffer[MAX_DATA_SIZE]="";
|
|
char *value;
|
|
char validation[256] = "";
|
|
|
|
queryid++;
|
|
packetnumber = 0;
|
|
|
|
for (querytype = qtbasic; querytype <= qtsecure ; (*(int *)(&querytype))++)
|
|
{
|
|
if ((value = value_for_key(query, queries[querytype])) != 0)
|
|
switch (querytype)
|
|
{
|
|
case qtbasic:
|
|
send_basic(sender,buffer);
|
|
break;
|
|
case qtinfo:
|
|
send_info(sender,buffer);
|
|
break;
|
|
case qtrules:
|
|
send_rules(sender,buffer);
|
|
break;
|
|
case qtplayers:
|
|
send_players(sender,buffer);
|
|
break;
|
|
case qtstatus:
|
|
send_basic(sender,buffer);
|
|
send_info(sender,buffer);
|
|
send_rules(sender,buffer);
|
|
send_players(sender,buffer);
|
|
break;
|
|
case qtpackets:
|
|
/*note: "packets" is NOT a real query type. It is simply here to illustrate
|
|
how a large query would look if broken into packets */
|
|
send_basic(sender,buffer); packet_send(sender, buffer);
|
|
send_info(sender,buffer); packet_send(sender, buffer);
|
|
send_rules(sender,buffer); packet_send(sender, buffer);
|
|
send_players(sender,buffer);
|
|
break;
|
|
case qtecho:
|
|
//note: \echo\value is the syntax here
|
|
send_echo(sender,buffer,value);
|
|
break;
|
|
case qtsecure:
|
|
strcpy(validation, value);
|
|
break;
|
|
case qtunknown:
|
|
break;
|
|
|
|
}
|
|
}
|
|
send_final(sender,buffer,validation);
|
|
}
|
|
|
|
/* send_heartbeat: Sends a heartbeat to the gamemaster,
|
|
adds \statechanged\ if statechanged != 0 */
|
|
static void send_heartbeat(int statechanged)
|
|
{
|
|
char buf[256];
|
|
int ret;
|
|
|
|
sprintf(buf,"\\heartbeat\\%d\\gamename\\%s",qport, gname);
|
|
if (statechanged)
|
|
strcat(buf,"\\statechanged\\");
|
|
#ifdef _DEBUG
|
|
OutputDebugString("send: ");
|
|
OutputDebugString(buf);
|
|
OutputDebugString("\n");
|
|
#endif
|
|
ret = send(hbsock, buf, strlen(buf), 0);
|
|
if (ret < 0) /* try to reconnect (tcp only) */
|
|
{
|
|
closesocket(hbsock);
|
|
hbsock = socket(PF_INET, SOCK_STREAM, 0);
|
|
|
|
int nGameSpyPort;
|
|
GameSpyOld::GetGameSpyAddress(GameSpyOld::szGameSpyAddress, 256);
|
|
nGameSpyPort = GameSpyOld::GetHeartbeatPort();
|
|
|
|
if (do_connect(hbsock, GameSpyOld::szGameSpyAddress, nGameSpyPort) == 0)
|
|
{
|
|
#ifdef _DEBUG
|
|
OutputDebugString("send: ");
|
|
OutputDebugString(buf);
|
|
OutputDebugString("\n");
|
|
#endif
|
|
send(hbsock, buf, strlen(buf), 0); /* try again */
|
|
}
|
|
}
|
|
|
|
lastheartbeat = current_time();
|
|
}
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
#endif |