// client.cpp, mostly network related client game code
#include "cube.h"
ENetHost *clienthost = NULL;
int connecting = 0;
int connattempts = 0;
int disconnecting = 0;
int clientnum = -1; // our client id in the game
bool c2sinit = false; // whether we need to tell the other clients our stats
int
getclientnum()
{
return clientnum;
}
bool
multiplayer()
{
// check not correct on listen server?
if (clienthost)
conoutf(@"operation not available in multiplayer");
return clienthost != NULL;
}
bool
allowedittoggle()
{
bool allow = !clienthost || gamemode == 1;
if (!allow)
conoutf(@"editing in multiplayer requires coopedit mode (1)");
return allow;
}
VARF(rate, 0, 0, 25000,
if (clienthost && (!rate || rate > 1000))
enet_host_bandwidth_limit(clienthost, rate, rate));
void throttle();
VARF(throttle_interval, 0, 5, 30, throttle());
VARF(throttle_accel, 0, 2, 32, throttle());
VARF(throttle_decel, 0, 2, 32, throttle());
void
throttle()
{
if (!clienthost || connecting)
return;
assert(ENET_PEER_PACKET_THROTTLE_SCALE == 32);
enet_peer_throttle_configure(clienthost->peers,
throttle_interval * 1000, throttle_accel, throttle_decel);
}
void
newname(OFString *name)
{
c2sinit = false;
@autoreleasepool {
strn0cpy(player1->name, name.UTF8String, 16);
}
}
COMMANDN(name, newname, ARG_1STR)
void
newteam(OFString *name)
{
c2sinit = false;
@autoreleasepool {
strn0cpy(player1->team, name.UTF8String, 5);
}
}
COMMANDN(team, newteam, ARG_1STR)
void
writeclientinfo(OFStream *stream)
{
[stream writeFormat:@"name \"%s\"\nteam \"%s\"\n", player1->name,
player1->team];
}
void
connects(OFString *servername)
{
disconnect(1); // reset state
addserver(servername);
conoutf(@"attempting to connect to %@", servername);
ENetAddress address = {ENET_HOST_ANY, CUBE_SERVER_PORT};
@autoreleasepool {
if (enet_address_set_host(&address, servername.UTF8String) <
0) {
conoutf(@"could not resolve server %@", servername);
return;
}
}
clienthost = enet_host_create(NULL, 1, rate, rate);
if (clienthost) {
enet_host_connect(clienthost, &address, 1);
enet_host_flush(clienthost);
connecting = lastmillis;
connattempts = 0;
} else {
conoutf(@"could not connect to server");
disconnect();
}
}
void
disconnect(int onlyclean, int async)
{
if (clienthost) {
if (!connecting && !disconnecting) {
enet_peer_disconnect(clienthost->peers);
enet_host_flush(clienthost);
disconnecting = lastmillis;
};
if (clienthost->peers->state != ENET_PEER_STATE_DISCONNECTED) {
if (async)
return;
enet_peer_reset(clienthost->peers);
};
enet_host_destroy(clienthost);
};
if (clienthost && !connecting)
conoutf(@"disconnected");
clienthost = NULL;
connecting = 0;
connattempts = 0;
disconnecting = 0;
clientnum = -1;
c2sinit = false;
player1->lifesequence = 0;
loopv(players) zapdynent(players[i]);
localdisconnect();
if (!onlyclean) {
stop();
localconnect();
}
}
void
trydisconnect()
{
if (!clienthost) {
conoutf(@"not connected");
return;
}
if (connecting) {
conoutf(@"aborting connection attempt");
disconnect();
return;
}
conoutf(@"attempting to disconnect...");
disconnect(0, !disconnecting);
}
static OFString *ctext;
void
toserver(OFString *text)
{
conoutf(@"%s:\f %@", player1->name, text);
ctext = text;
}
void
echo(OFString *text)
{
conoutf(@"%@", text);
}
COMMAND(echo, ARG_VARI)
COMMANDN(say, toserver, ARG_VARI)
COMMANDN(connect, connects, ARG_1STR)
COMMANDN(disconnect, trydisconnect, ARG_NONE)
// collect c2s messages conveniently
static OFMutableArray<OFData *> *messages;
void
addmsg(int rel, int num, int type, ...)
{
if (demoplayback)
return;
if (num != msgsizelookup(type)) {
fatal([OFString
stringWithFormat:@"inconsistant msg size for %d (%d != %d)",
type, num, msgsizelookup(type)]);
}
if (messages.count == 100) {
conoutf(@"command flood protection (type %d)", type);
return;
}
OFMutableData *msg = [OFMutableData dataWithItemSize:sizeof(int)
capacity:num + 2];
[msg addItem:&num];
[msg addItem:&rel];
[msg addItem:&type];
va_list marker;
va_start(marker, type);
loopi(num - 1)
{
int tmp = va_arg(marker, int);
[msg addItem:&tmp];
}
va_end(marker);
[msg makeImmutable];
if (messages == nil)
messages = [[OFMutableArray alloc] init];
[messages addObject:msg];
}
void
server_err()
{
conoutf(@"server network error, disconnecting...");
disconnect();
}
int lastupdate = 0, lastping = 0;
OFString *toservermap;
bool senditemstoserver =
false; // after a map change, since server doesn't have map data
OFString *clientpassword;
void
password(OFString *p)
{
clientpassword = p;
}
COMMAND(password, ARG_1STR)
bool
netmapstart()
{
senditemstoserver = true;
return clienthost != NULL;
}
void
initclientnet()
{
ctext = @"";
toservermap = @"";
clientpassword = @"";
newname(@"unnamed");
newteam(@"red");
}
void
sendpackettoserv(void *packet)
{
if (clienthost) {
enet_host_broadcast(clienthost, 0, (ENetPacket *)packet);
enet_host_flush(clienthost);
} else
localclienttoserver((ENetPacket *)packet);
}
void
c2sinfo(dynent *d) // send update to the server
{
@autoreleasepool {
if (clientnum < 0)
return; // we haven't had a welcome message from the
// server yet
if (lastmillis - lastupdate < 40)
return; // don't update faster than 25fps
ENetPacket *packet = enet_packet_create(NULL, MAXTRANS, 0);
uchar *start = packet->data;
uchar *p = start + 2;
bool serveriteminitdone = false;
if (toservermap.length > 0) // suggest server to change map
{ // do this exclusively as map change may invalidate rest of
// update
packet->flags = ENET_PACKET_FLAG_RELIABLE;
putint(p, SV_MAPCHANGE);
sendstring(toservermap, p);
toservermap = @"";
putint(p, nextmode);
} else {
putint(p, SV_POS);
putint(p, clientnum);
putint(
p, (int)(d->o.x *
DMF)); // quantize coordinates to 1/16th
// of a cube, between 1 and 3 bytes
putint(p, (int)(d->o.y * DMF));
putint(p, (int)(d->o.z * DMF));
putint(p, (int)(d->yaw * DAF));
putint(p, (int)(d->pitch * DAF));
putint(p, (int)(d->roll * DAF));
putint(
p, (int)(d->vel.x * DVF)); // quantize to 1/100,
// almost always 1 byte
putint(p, (int)(d->vel.y * DVF));
putint(p, (int)(d->vel.z * DVF));
// pack rest in 1 byte: strafe:2, move:2, onfloor:1,
// state:3
putint(
p, (d->strafe & 3) | ((d->move & 3) << 2) |
(((int)d->onfloor) << 4) |
((editmode ? CS_EDITING : d->state) << 5));
if (senditemstoserver) {
packet->flags = ENET_PACKET_FLAG_RELIABLE;
putint(p, SV_ITEMLIST);
if (!m_noitems)
putitems(p);
putint(p, -1);
senditemstoserver = false;
serveriteminitdone = true;
}
// player chat, not flood protected for now
if (ctext.length > 0) {
packet->flags = ENET_PACKET_FLAG_RELIABLE;
putint(p, SV_TEXT);
sendstring(ctext, p);
ctext = @"";
}
if (!c2sinit) // tell other clients who I am
{
packet->flags = ENET_PACKET_FLAG_RELIABLE;
c2sinit = true;
putint(p, SV_INITC2S);
sendstring(@(player1->name), p);
sendstring(@(player1->team), p);
putint(p, player1->lifesequence);
}
for (OFData *msg in messages) {
// send messages collected during the previous
// frames
if (*(int *)[msg itemAtIndex:1])
packet->flags =
ENET_PACKET_FLAG_RELIABLE;
loopi(*(int *)[msg itemAtIndex:0])
putint(p, *(int *)[msg itemAtIndex:i + 2]);
}
[messages removeAllObjects];
if (lastmillis - lastping > 250) {
putint(p, SV_PING);
putint(p, lastmillis);
lastping = lastmillis;
}
}
*(ushort *)start = ENET_HOST_TO_NET_16(p - start);
enet_packet_resize(packet, p - start);
incomingdemodata(start, p - start, true);
if (clienthost) {
enet_host_broadcast(clienthost, 0, packet);
enet_host_flush(clienthost);
} else
localclienttoserver(packet);
lastupdate = lastmillis;
if (serveriteminitdone)
loadgamerest(); // hack
}
}
void
gets2c() // get updates from the server
{
ENetEvent event;
if (!clienthost)
return;
if (connecting && lastmillis / 3000 > connecting / 3000) {
conoutf(@"attempting to connect...");
connecting = lastmillis;
++connattempts;
if (connattempts > 3) {
conoutf(@"could not connect to server");
disconnect();
return;
};
};
while (
clienthost != NULL && enet_host_service(clienthost, &event, 0) > 0)
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
conoutf(@"connected to server");
connecting = 0;
throttle();
break;
case ENET_EVENT_TYPE_RECEIVE:
if (disconnecting)
conoutf(@"attempting to disconnect...");
else
localservertoclient(event.packet->data,
event.packet->dataLength);
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
if (disconnecting)
disconnect();
else
server_err();
return;
}
};