ADDED src/Client.m Index: src/Client.m ================================================================== --- /dev/null +++ src/Client.m @@ -0,0 +1,8 @@ +#import "Client.h" + +@implementation Client ++ (instancetype)client +{ + return [[self alloc] init]; +} +@end DELETED src/client.m Index: src/client.m ================================================================== --- src/client.m +++ /dev/null @@ -1,8 +0,0 @@ -#import "Client.h" - -@implementation Client -+ (instancetype)client -{ - return [[self alloc] init]; -} -@end ADDED src/clientextras.m Index: src/clientextras.m ================================================================== --- /dev/null +++ src/clientextras.m @@ -0,0 +1,226 @@ +// clientextras.cpp: stuff that didn't fit in client.cpp or clientgame.cpp :) + +#include "cube.h" + +#import "DynamicEntity.h" + +// render players & monsters +// very messy ad-hoc handling of animation frames, should be made more +// configurable + +// D D D D' D D D D' A A' P P' I I' +// R, R' E L J J' +int frame[] = { 178, 184, 190, 137, 183, 189, 197, 164, 46, 51, 54, 32, 0, 0, + 40, 1, 162, 162, 67, 168 }; +int range[] = { 6, 6, 8, 28, 1, 1, 1, 1, 8, 19, 4, 18, 40, 1, 6, 15, 1, 1, 1, + 1 }; + +void +renderclient( + DynamicEntity *d, bool team, OFString *mdlname, bool hellpig, float scale) +{ + int n = 3; + float speed = 100.0f; + float mz = d.o.z - d.eyeheight + 1.55f * scale; + int basetime = -((intptr_t)d & 0xFFF); + if (d.state == CS_DEAD) { + int r; + if (hellpig) { + n = 2; + r = range[3]; + } else { + n = (intptr_t)d % 3; + r = range[n]; + } + basetime = d.lastaction; + int t = lastmillis - d.lastaction; + if (t < 0 || t > 20000) + return; + if (t > (r - 1) * 100) { + n += 4; + if (t > (r + 10) * 100) { + t -= (r + 10) * 100; + mz -= t * t / 10000000000.0f * t; + } + } + if (mz < -1000) + return; + // mdl = (((int)d>>6)&1)+1; + // mz = d.o.z-d.eyeheight+0.2f; + // scale = 1.2f; + } else if (d.state == CS_EDITING) { + n = 16; + } else if (d.state == CS_LAGGED) { + n = 17; + } else if (d.monsterstate == M_ATTACKING) { + n = 8; + } else if (d.monsterstate == M_PAIN) { + n = 10; + } else if ((!d.move && !d.strafe) || !d.moving) { + n = 12; + } else if (!d.onfloor && d.timeinair > 100) { + n = 18; + } else { + n = 14; + speed = 1200 / d.maxspeed * scale; + if (hellpig) + speed = 300 / d.maxspeed; + } + if (hellpig) { + n++; + scale *= 32; + mz -= 1.9f; + } + rendermodel(mdlname, frame[n], range[n], 0, 1.5f, + OFMakeVector3D(d.o.x, mz, d.o.y), d.yaw + 90, d.pitch / 2, team, + scale, speed, 0, basetime); +} + +extern int democlientnum; + +void +renderclients() +{ + [players enumerateObjectsUsingBlock:^(id player, size_t i, bool *stop) { + if (player != [OFNull null] && + (!demoplayback || i != democlientnum)) + renderclient(player, + isteam(player1.team, [player team]), + @"monster/ogro", false, 1.0f); + }]; +} + +// creation of scoreboard pseudo-menu + +bool scoreson = false; + +void +showscores(bool on) +{ + scoreson = on; + menuset(((int)on) - 1); +} + +static OFMutableArray *scoreLines; + +void +renderscore(DynamicEntity *d) +{ + OFString *lag = [OFString stringWithFormat:@"%d", d.plag]; + OFString *name = [OFString stringWithFormat:@"(%@)", d.name]; + OFString *line = + [OFString stringWithFormat:@"%d\t%@\t%d\t%@\t%@", d.frags, + (d.state == CS_LAGGED ? @"LAG" : lag), d.ping, d.team, + (d.state == CS_DEAD ? name : d.name)]; + + if (scoreLines == nil) + scoreLines = [[OFMutableArray alloc] init]; + + [scoreLines addObject:line]; + + menumanual(0, scoreLines.count - 1, line); +} + +static const int maxTeams = 4; +static OFString *teamName[maxTeams]; +static int teamScore[maxTeams]; +static size_t teamsUsed; + +void +addteamscore(DynamicEntity *d) +{ + for (size_t i = 0; i < teamsUsed; i++) { + if ([teamName[i] isEqual:d.team]) { + teamScore[i] += d.frags; + return; + } + } + + if (teamsUsed == maxTeams) + return; + + teamName[teamsUsed] = d.team; + teamScore[teamsUsed++] = d.frags; +} + +void +renderscores() +{ + if (!scoreson) + return; + [scoreLines removeAllObjects]; + if (!demoplayback) + renderscore(player1); + for (id player in players) + if (player != [OFNull null]) + renderscore(player); + sortmenu(); + if (m_teammode) { + teamsUsed = 0; + for (id player in players) + if (player != [OFNull null]) + addteamscore(player); + if (!demoplayback) + addteamscore(player1); + OFMutableString *teamScores = [OFMutableString string]; + for (size_t j = 0; j < teamsUsed; j++) + [teamScores appendFormat:@"[ %@: %d ]", teamName[j], + teamScore[j]]; + menumanual(0, scoreLines.count, @""); + menumanual(0, scoreLines.count + 1, teamScores); + } +} + +// sendmap/getmap commands, should be replaced by more intuitive map downloading + +void +sendmap(OFString *mapname) +{ + if (mapname.length > 0) + save_world(mapname); + changemap(mapname); + mapname = getclientmap(); + OFData *mapdata = readmap(mapname); + if (mapdata == nil) + return; + ENetPacket *packet = enet_packet_create( + NULL, MAXTRANS + mapdata.count, ENET_PACKET_FLAG_RELIABLE); + uchar *start = packet->data; + uchar *p = start + 2; + putint(&p, SV_SENDMAP); + sendstring(mapname, &p); + putint(&p, mapdata.count); + if (65535 - (p - start) < mapdata.count) { + conoutf(@"map %@ is too large to send", mapname); + enet_packet_destroy(packet); + return; + } + memcpy(p, mapdata.items, mapdata.count); + p += mapdata.count; + *(ushort *)start = ENET_HOST_TO_NET_16(p - start); + enet_packet_resize(packet, p - start); + sendpackettoserv(packet); + conoutf(@"sending map %@ to server...", mapname); + OFString *msg = + [OFString stringWithFormat:@"[map %@ uploaded to server, " + @"\"getmap\" to receive it]", + mapname]; + toserver(msg); +} + +void +getmap() +{ + ENetPacket *packet = + enet_packet_create(NULL, MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + uchar *start = packet->data; + uchar *p = start + 2; + putint(&p, SV_RECVMAP); + *(ushort *)start = ENET_HOST_TO_NET_16(p - start); + enet_packet_resize(packet, p - start); + sendpackettoserv(packet); + conoutf(@"requesting map from server..."); +} + +COMMAND(sendmap, ARG_1STR) +COMMAND(getmap, ARG_NONE) DELETED src/clientextras.mm Index: src/clientextras.mm ================================================================== --- src/clientextras.mm +++ /dev/null @@ -1,226 +0,0 @@ -// clientextras.cpp: stuff that didn't fit in client.cpp or clientgame.cpp :) - -#include "cube.h" - -#import "DynamicEntity.h" - -// render players & monsters -// very messy ad-hoc handling of animation frames, should be made more -// configurable - -// D D D D' D D D D' A A' P P' I I' -// R, R' E L J J' -int frame[] = { 178, 184, 190, 137, 183, 189, 197, 164, 46, 51, 54, 32, 0, 0, - 40, 1, 162, 162, 67, 168 }; -int range[] = { 6, 6, 8, 28, 1, 1, 1, 1, 8, 19, 4, 18, 40, 1, 6, 15, 1, 1, 1, - 1 }; - -void -renderclient( - DynamicEntity *d, bool team, OFString *mdlname, bool hellpig, float scale) -{ - int n = 3; - float speed = 100.0f; - float mz = d.o.z - d.eyeheight + 1.55f * scale; - int basetime = -((intptr_t)d & 0xFFF); - if (d.state == CS_DEAD) { - int r; - if (hellpig) { - n = 2; - r = range[3]; - } else { - n = (intptr_t)d % 3; - r = range[n]; - } - basetime = d.lastaction; - int t = lastmillis - d.lastaction; - if (t < 0 || t > 20000) - return; - if (t > (r - 1) * 100) { - n += 4; - if (t > (r + 10) * 100) { - t -= (r + 10) * 100; - mz -= t * t / 10000000000.0f * t; - } - } - if (mz < -1000) - return; - // mdl = (((int)d>>6)&1)+1; - // mz = d.o.z-d.eyeheight+0.2f; - // scale = 1.2f; - } else if (d.state == CS_EDITING) { - n = 16; - } else if (d.state == CS_LAGGED) { - n = 17; - } else if (d.monsterstate == M_ATTACKING) { - n = 8; - } else if (d.monsterstate == M_PAIN) { - n = 10; - } else if ((!d.move && !d.strafe) || !d.moving) { - n = 12; - } else if (!d.onfloor && d.timeinair > 100) { - n = 18; - } else { - n = 14; - speed = 1200 / d.maxspeed * scale; - if (hellpig) - speed = 300 / d.maxspeed; - } - if (hellpig) { - n++; - scale *= 32; - mz -= 1.9f; - } - rendermodel(mdlname, frame[n], range[n], 0, 1.5f, - OFMakeVector3D(d.o.x, mz, d.o.y), d.yaw + 90, d.pitch / 2, team, - scale, speed, 0, basetime); -} - -extern int democlientnum; - -void -renderclients() -{ - [players enumerateObjectsUsingBlock:^(id player, size_t i, bool *stop) { - if (player != [OFNull null] && - (!demoplayback || i != democlientnum)) - renderclient(player, - isteam(player1.team, [player team]), - @"monster/ogro", false, 1.0f); - }]; -} - -// creation of scoreboard pseudo-menu - -bool scoreson = false; - -void -showscores(bool on) -{ - scoreson = on; - menuset(((int)on) - 1); -} - -static OFMutableArray *scoreLines; - -void -renderscore(DynamicEntity *d) -{ - OFString *lag = [OFString stringWithFormat:@"%d", d.plag]; - OFString *name = [OFString stringWithFormat:@"(%@)", d.name]; - OFString *line = - [OFString stringWithFormat:@"%d\t%@\t%d\t%@\t%@", d.frags, - (d.state == CS_LAGGED ? @"LAG" : lag), d.ping, d.team, - (d.state == CS_DEAD ? name : d.name)]; - - if (scoreLines == nil) - scoreLines = [[OFMutableArray alloc] init]; - - [scoreLines addObject:line]; - - menumanual(0, scoreLines.count - 1, line); -} - -static const int maxTeams = 4; -static OFString *teamName[maxTeams]; -static int teamScore[maxTeams]; -static size_t teamsUsed; - -void -addteamscore(DynamicEntity *d) -{ - for (size_t i = 0; i < teamsUsed; i++) { - if ([teamName[i] isEqual:d.team]) { - teamScore[i] += d.frags; - return; - } - } - - if (teamsUsed == maxTeams) - return; - - teamName[teamsUsed] = d.team; - teamScore[teamsUsed++] = d.frags; -} - -void -renderscores() -{ - if (!scoreson) - return; - [scoreLines removeAllObjects]; - if (!demoplayback) - renderscore(player1); - for (id player in players) - if (player != [OFNull null]) - renderscore(player); - sortmenu(); - if (m_teammode) { - teamsUsed = 0; - for (id player in players) - if (player != [OFNull null]) - addteamscore(player); - if (!demoplayback) - addteamscore(player1); - OFMutableString *teamScores = [OFMutableString string]; - for (size_t j = 0; j < teamsUsed; j++) - [teamScores appendFormat:@"[ %@: %d ]", teamName[j], - teamScore[j]]; - menumanual(0, scoreLines.count, @""); - menumanual(0, scoreLines.count + 1, teamScores); - } -} - -// sendmap/getmap commands, should be replaced by more intuitive map downloading - -void -sendmap(OFString *mapname) -{ - if (mapname.length > 0) - save_world(mapname); - changemap(mapname); - mapname = getclientmap(); - OFData *mapdata = readmap(mapname); - if (mapdata == nil) - return; - ENetPacket *packet = enet_packet_create( - NULL, MAXTRANS + mapdata.count, ENET_PACKET_FLAG_RELIABLE); - uchar *start = packet->data; - uchar *p = start + 2; - putint(&p, SV_SENDMAP); - sendstring(mapname, &p); - putint(&p, mapdata.count); - if (65535 - (p - start) < mapdata.count) { - conoutf(@"map %@ is too large to send", mapname); - enet_packet_destroy(packet); - return; - } - memcpy(p, mapdata.items, mapdata.count); - p += mapdata.count; - *(ushort *)start = ENET_HOST_TO_NET_16(p - start); - enet_packet_resize(packet, p - start); - sendpackettoserv(packet); - conoutf(@"sending map %@ to server...", mapname); - OFString *msg = - [OFString stringWithFormat:@"[map %@ uploaded to server, " - @"\"getmap\" to receive it]", - mapname]; - toserver(msg); -} - -void -getmap() -{ - ENetPacket *packet = - enet_packet_create(NULL, MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - uchar *start = packet->data; - uchar *p = start + 2; - putint(&p, SV_RECVMAP); - *(ushort *)start = ENET_HOST_TO_NET_16(p - start); - enet_packet_resize(packet, p - start); - sendpackettoserv(packet); - conoutf(@"requesting map from server..."); -} - -COMMAND(sendmap, ARG_1STR) -COMMAND(getmap, ARG_NONE) ADDED src/clientgame.m Index: src/clientgame.m ================================================================== --- /dev/null +++ src/clientgame.m @@ -0,0 +1,528 @@ +// clientgame.cpp: core game related stuff + +#include "cube.h" + +#import "DynamicEntity.h" +#import "Entity.h" +#import "OFString+Cube.h" + +int nextmode = 0; // nextmode becomes gamemode after next map load +VAR(gamemode, 1, 0, 0); + +static void +mode(int n) +{ + addmsg(1, 2, SV_GAMEMODE, nextmode = n); +} +COMMAND(mode, ARG_1INT) + +bool intermission = false; + +DynamicEntity *player1; // our client +OFMutableArray *players; // other clients + +void +initPlayers() +{ + player1 = newdynent(); + players = [[OFMutableArray alloc] init]; +} + +VARP(sensitivity, 0, 10, 10000); +VARP(sensitivityscale, 1, 1, 10000); +VARP(invmouse, 0, 0, 1); + +int lastmillis = 0; +int curtime = 10; +OFString *clientmap; + +OFString * +getclientmap() +{ + return clientmap; +} + +void +resetmovement(DynamicEntity *d) +{ + d.k_left = false; + d.k_right = false; + d.k_up = false; + d.k_down = false; + d.jumpnext = false; + d.strafe = 0; + d.move = 0; +} + +// reset player state not persistent accross spawns +void +spawnstate(DynamicEntity *d) +{ + resetmovement(d); + d.vel = OFMakeVector3D(0, 0, 0); + d.onfloor = false; + d.timeinair = 0; + d.health = 100; + d.armour = 50; + d.armourtype = A_BLUE; + d.quadmillis = 0; + d.lastattackgun = d.gunselect = GUN_SG; + d.gunwait = 0; + d.attacking = false; + d.lastaction = 0; + loopi(NUMGUNS) d.ammo[i] = 0; + d.ammo[GUN_FIST] = 1; + if (m_noitems) { + d.gunselect = GUN_RIFLE; + d.armour = 0; + if (m_noitemsrail) { + d.health = 1; + d.ammo[GUN_RIFLE] = 100; + } else { + if (gamemode == 12) { + // eihrul's secret "instafist" mode + d.gunselect = GUN_FIST; + return; + } + d.health = 256; + if (m_tarena) { + int gun1 = rnd(4) + 1; + baseammo(d.gunselect = gun1); + for (;;) { + int gun2 = rnd(4) + 1; + if (gun1 != gun2) { + baseammo(gun2); + break; + } + } + } else if (m_arena) { + // insta arena + d.ammo[GUN_RIFLE] = 100; + } else { + // efficiency + loopi(4) baseammo(i + 1); + d.gunselect = GUN_CG; + } + d.ammo[GUN_CG] /= 2; + } + } else + d.ammo[GUN_SG] = 5; +} + +DynamicEntity * +newdynent() // create a new blank player or monster +{ + DynamicEntity *d = [[DynamicEntity alloc] init]; + d.o = OFMakeVector3D(0, 0, 0); + d.yaw = 270; + d.pitch = 0; + d.roll = 0; + d.maxspeed = 22; + d.outsidemap = false; + d.inwater = false; + d.radius = 1.1f; + d.eyeheight = 3.2f; + d.aboveeye = 0.7f; + d.frags = 0; + d.plag = 0; + d.ping = 0; + d.lastupdate = lastmillis; + d.enemy = NULL; + d.monsterstate = 0; + d.name = d.team = @""; + d.blocked = false; + d.lifesequence = 0; + d.state = CS_ALIVE; + spawnstate(d); + return d; +} + +void +respawnself() +{ + spawnplayer(player1); + showscores(false); +} + +static void +arenacount( + DynamicEntity *d, int *alive, int *dead, OFString **lastteam, bool *oneteam) +{ + if (d.state != CS_DEAD) { + (*alive)++; + if (![*lastteam isEqual:d.team]) + *oneteam = false; + *lastteam = d.team; + } else + (*dead)++; +} + +int arenarespawnwait = 0; +int arenadetectwait = 0; + +void +arenarespawn() +{ + if (arenarespawnwait) { + if (arenarespawnwait < lastmillis) { + arenarespawnwait = 0; + conoutf(@"new round starting... fight!"); + respawnself(); + } + } else if (arenadetectwait == 0 || arenadetectwait < lastmillis) { + arenadetectwait = 0; + int alive = 0, dead = 0; + OFString *lastteam = nil; + bool oneteam = true; + for (id player in players) + if (player != [OFNull null]) + arenacount( + player, &alive, &dead, &lastteam, &oneteam); + arenacount(player1, &alive, &dead, &lastteam, &oneteam); + if (dead > 0 && (alive <= 1 || (m_teammode && oneteam))) { + conoutf( + @"arena round is over! next round in 5 seconds..."); + if (alive) + conoutf( + @"team %s is last man standing", lastteam); + else + conoutf(@"everyone died!"); + arenarespawnwait = lastmillis + 5000; + arenadetectwait = lastmillis + 10000; + player1.roll = 0; + } + } +} + +extern int democlientnum; + +void +otherplayers() +{ + [players enumerateObjectsUsingBlock:^(id player, size_t i, bool *stop) { + if (player == [OFNull null]) + return; + + const int lagtime = lastmillis - [player lastupdate]; + if (lagtime > 1000 && [player state] == CS_ALIVE) { + [player setState:CS_LAGGED]; + return; + } + + if (lagtime && [player state] != CS_DEAD && + (!demoplayback || i != democlientnum)) + // use physics to extrapolate player position + moveplayer(player, 2, false); + }]; +} + +void +respawn() +{ + if (player1.state == CS_DEAD) { + player1.attacking = false; + if (m_arena) { + conoutf(@"waiting for new round to start..."); + return; + } + if (m_sp) { + nextmode = gamemode; + changemap(clientmap); + return; + } // if we die in SP we try the same map again + respawnself(); + } +} + +int sleepwait = 0; +static OFString *sleepcmd = nil; +void +sleepf(OFString *msec, OFString *cmd) +{ + sleepwait = msec.cube_intValue + lastmillis; + sleepcmd = cmd; +} +COMMANDN(sleep, sleepf, ARG_2STR) + +void +updateworld(int millis) // main game update loop +{ + if (lastmillis) { + curtime = millis - lastmillis; + if (sleepwait && lastmillis > sleepwait) { + sleepwait = 0; + execute(sleepcmd, true); + } + physicsframe(); + checkquad(curtime); + if (m_arena) + arenarespawn(); + moveprojectiles((float)curtime); + demoplaybackstep(); + if (!demoplayback) { + if (getclientnum() >= 0) + // only shoot when connected to server + shoot(player1, &worldpos); + // do this first, so we have most accurate information + // when our player moves + gets2c(); + } + otherplayers(); + if (!demoplayback) { + monsterthink(); + if (player1.state == CS_DEAD) { + if (lastmillis - player1.lastaction < 2000) { + player1.move = player1.strafe = 0; + moveplayer(player1, 10, false); + } else if (!m_arena && !m_sp && + lastmillis - player1.lastaction > 10000) + respawn(); + } else if (!intermission) { + moveplayer(player1, 20, true); + checkitems(); + } + // do this last, to reduce the effective frame lag + c2sinfo(player1); + } + } + lastmillis = millis; +} + +// brute force but effective way to find a free spawn spot in the map +void +entinmap(DynamicEntity *d) +{ + loopi(100) // try max 100 times + { + float dx = (rnd(21) - 10) / 10.0f * i; // increasing distance + float dy = (rnd(21) - 10) / 10.0f * i; + OFVector3D old = d.o; + d.o = OFMakeVector3D(d.o.x + dx, d.o.y + dy, d.o.z); + if (collide(d, true, 0, 0)) + return; + d.o = old; + } + conoutf( + @"can't find entity spawn spot! (%d, %d)", (int)d.o.x, (int)d.o.y); + // leave ent at original pos, possibly stuck +} + +int spawncycle = -1; +int fixspawn = 2; + +// place at random spawn. also used by monsters! +void +spawnplayer(DynamicEntity *d) +{ + int r = fixspawn-- > 0 ? 4 : rnd(10) + 1; + loopi(r) spawncycle = findentity(PLAYERSTART, spawncycle + 1); + if (spawncycle != -1) { + d.o = OFMakeVector3D( + ents[spawncycle].x, ents[spawncycle].y, ents[spawncycle].z); + d.yaw = ents[spawncycle].attr1; + d.pitch = 0; + d.roll = 0; + } else + d.o = OFMakeVector3D((float)ssize / 2, (float)ssize / 2, 4); + entinmap(d); + spawnstate(d); + d.state = CS_ALIVE; +} + +// movement input code + +#define dir(name, v, d, s, os) \ + static void name(bool isdown) \ + { \ + player1.s = isdown; \ + player1.v = isdown ? d : (player1.os ? -(d) : 0); \ + player1.lastmove = lastmillis; \ + } + +dir(backward, move, -1, k_down, k_up); +dir(forward, move, 1, k_up, k_down); +dir(left, strafe, 1, k_left, k_right); +dir(right, strafe, -1, k_right, k_left); + +void +attack(bool on) +{ + if (intermission) + return; + if (editmode) + editdrag(on); + else if ((player1.attacking = on)) + respawn(); +} + +void +jumpn(bool on) +{ + if (!intermission && (player1.jumpnext = on)) + respawn(); +} + +COMMAND(backward, ARG_DOWN) +COMMAND(forward, ARG_DOWN) +COMMAND(left, ARG_DOWN) +COMMAND(right, ARG_DOWN) +COMMANDN(jump, jumpn, ARG_DOWN) +COMMAND(attack, ARG_DOWN) +COMMAND(showscores, ARG_DOWN) + +void +fixplayer1range() +{ + const float MAXPITCH = 90.0f; + if (player1.pitch > MAXPITCH) + player1.pitch = MAXPITCH; + if (player1.pitch < -MAXPITCH) + player1.pitch = -MAXPITCH; + while (player1.yaw < 0.0f) + player1.yaw += 360.0f; + while (player1.yaw >= 360.0f) + player1.yaw -= 360.0f; +} + +void +mousemove(int dx, int dy) +{ + if (player1.state == CS_DEAD || intermission) + return; + const float SENSF = 33.0f; // try match quake sens + player1.yaw += (dx / SENSF) * (sensitivity / (float)sensitivityscale); + player1.pitch -= (dy / SENSF) * + (sensitivity / (float)sensitivityscale) * (invmouse ? -1 : 1); + fixplayer1range(); +} + +// damage arriving from the network, monsters, yourself, all ends up here. + +void +selfdamage(int damage, int actor, DynamicEntity *act) +{ + if (player1.state != CS_ALIVE || editmode || intermission) + return; + damageblend(damage); + demoblend(damage); + // let armour absorb when possible + int ad = damage * (player1.armourtype + 1) * 20 / 100; + if (ad > player1.armour) + ad = player1.armour; + player1.armour -= ad; + damage -= ad; + float droll = damage / 0.5f; + player1.roll += player1.roll > 0 + ? droll + : (player1.roll < 0 + ? -droll + : (rnd(2) ? droll + : -droll)); // give player a kick depending + // on amount of damage + if ((player1.health -= damage) <= 0) { + if (actor == -2) { + conoutf(@"you got killed by %@!", act.name); + } else if (actor == -1) { + actor = getclientnum(); + conoutf(@"you suicided!"); + addmsg(1, 2, SV_FRAGS, --player1.frags); + } else { + DynamicEntity *a = getclient(actor); + if (a != nil) { + if (isteam(a.team, player1.team)) + conoutf(@"you got fragged by a " + @"teammate (%@)", + a.name); + else + conoutf( + @"you got fragged by %@", a.name); + } + } + showscores(true); + addmsg(1, 2, SV_DIED, actor); + player1.lifesequence++; + player1.attacking = false; + player1.state = CS_DEAD; + player1.pitch = 0; + player1.roll = 60; + playsound(S_DIE1 + rnd(2), NULL); + spawnstate(player1); + player1.lastaction = lastmillis; + } else + playsound(S_PAIN6, NULL); +} + +void +timeupdate(int timeremain) +{ + if (!timeremain) { + intermission = true; + player1.attacking = false; + conoutf(@"intermission:"); + conoutf(@"game has ended!"); + showscores(true); + } else { + conoutf(@"time remaining: %d minutes", timeremain); + } +} + +DynamicEntity * +getclient(int cn) // ensure valid entity +{ + if (cn < 0 || cn >= MAXCLIENTS) { + neterr(@"clientnum"); + return nil; + } + while (cn >= players.count) + [players addObject:[OFNull null]]; + return (players[cn] != [OFNull null] ? players[cn] + : (players[cn] = newdynent())); +} + +void +setclient(int cn, id client) +{ + if (cn < 0 || cn >= MAXCLIENTS) + neterr(@"clientnum"); + while (cn >= players.count) + [players addObject:[OFNull null]]; + players[cn] = client; +} + +void +initclient() +{ + clientmap = @""; + initclientnet(); +} + +void +startmap(OFString *name) // called just after a map load +{ + if (netmapstart() && m_sp) { + gamemode = 0; + conoutf(@"coop sp not supported yet"); + } + sleepwait = 0; + monsterclear(); + projreset(); + spawncycle = -1; + spawnplayer(player1); + player1.frags = 0; + for (id player in players) + if (player != [OFNull null]) + [player setFrags:0]; + resetspawns(); + clientmap = name; + if (editmode) + toggleedit(); + setvar(@"gamespeed", 100); + setvar(@"fog", 180); + setvar(@"fogcolour", 0x8099B3); + showscores(false); + intermission = false; + Cube.sharedInstance.framesInMap = 0; + conoutf(@"game mode is %@", modestr(gamemode)); +} + +COMMANDN(map, changemap, ARG_1STR) DELETED src/clientgame.mm Index: src/clientgame.mm ================================================================== --- src/clientgame.mm +++ /dev/null @@ -1,528 +0,0 @@ -// clientgame.cpp: core game related stuff - -#include "cube.h" - -#import "DynamicEntity.h" -#import "Entity.h" -#import "OFString+Cube.h" - -int nextmode = 0; // nextmode becomes gamemode after next map load -VAR(gamemode, 1, 0, 0); - -void -mode(int n) -{ - addmsg(1, 2, SV_GAMEMODE, nextmode = n); -} -COMMAND(mode, ARG_1INT) - -bool intermission = false; - -DynamicEntity *player1; // our client -OFMutableArray *players; // other clients - -void -initPlayers() -{ - player1 = newdynent(); - players = [[OFMutableArray alloc] init]; -} - -VARP(sensitivity, 0, 10, 10000); -VARP(sensitivityscale, 1, 1, 10000); -VARP(invmouse, 0, 0, 1); - -int lastmillis = 0; -int curtime = 10; -OFString *clientmap; - -OFString * -getclientmap() -{ - return clientmap; -} - -void -resetmovement(DynamicEntity *d) -{ - d.k_left = false; - d.k_right = false; - d.k_up = false; - d.k_down = false; - d.jumpnext = false; - d.strafe = 0; - d.move = 0; -} - -// reset player state not persistent accross spawns -void -spawnstate(DynamicEntity *d) -{ - resetmovement(d); - d.vel = OFMakeVector3D(0, 0, 0); - d.onfloor = false; - d.timeinair = 0; - d.health = 100; - d.armour = 50; - d.armourtype = A_BLUE; - d.quadmillis = 0; - d.lastattackgun = d.gunselect = GUN_SG; - d.gunwait = 0; - d.attacking = false; - d.lastaction = 0; - loopi(NUMGUNS) d.ammo[i] = 0; - d.ammo[GUN_FIST] = 1; - if (m_noitems) { - d.gunselect = GUN_RIFLE; - d.armour = 0; - if (m_noitemsrail) { - d.health = 1; - d.ammo[GUN_RIFLE] = 100; - } else { - if (gamemode == 12) { - // eihrul's secret "instafist" mode - d.gunselect = GUN_FIST; - return; - } - d.health = 256; - if (m_tarena) { - int gun1 = rnd(4) + 1; - baseammo(d.gunselect = gun1); - for (;;) { - int gun2 = rnd(4) + 1; - if (gun1 != gun2) { - baseammo(gun2); - break; - } - } - } else if (m_arena) { - // insta arena - d.ammo[GUN_RIFLE] = 100; - } else { - // efficiency - loopi(4) baseammo(i + 1); - d.gunselect = GUN_CG; - } - d.ammo[GUN_CG] /= 2; - } - } else - d.ammo[GUN_SG] = 5; -} - -DynamicEntity * -newdynent() // create a new blank player or monster -{ - DynamicEntity *d = [[DynamicEntity alloc] init]; - d.o = OFMakeVector3D(0, 0, 0); - d.yaw = 270; - d.pitch = 0; - d.roll = 0; - d.maxspeed = 22; - d.outsidemap = false; - d.inwater = false; - d.radius = 1.1f; - d.eyeheight = 3.2f; - d.aboveeye = 0.7f; - d.frags = 0; - d.plag = 0; - d.ping = 0; - d.lastupdate = lastmillis; - d.enemy = NULL; - d.monsterstate = 0; - d.name = d.team = @""; - d.blocked = false; - d.lifesequence = 0; - d.state = CS_ALIVE; - spawnstate(d); - return d; -} - -void -respawnself() -{ - spawnplayer(player1); - showscores(false); -} - -void -arenacount( - DynamicEntity *d, int &alive, int &dead, OFString **lastteam, bool &oneteam) -{ - if (d.state != CS_DEAD) { - alive++; - if (![*lastteam isEqual:d.team]) - oneteam = false; - *lastteam = d.team; - } else - dead++; -} - -int arenarespawnwait = 0; -int arenadetectwait = 0; - -void -arenarespawn() -{ - if (arenarespawnwait) { - if (arenarespawnwait < lastmillis) { - arenarespawnwait = 0; - conoutf(@"new round starting... fight!"); - respawnself(); - } - } else if (arenadetectwait == 0 || arenadetectwait < lastmillis) { - arenadetectwait = 0; - int alive = 0, dead = 0; - OFString *lastteam = nil; - bool oneteam = true; - for (id player in players) - if (player != [OFNull null]) - arenacount( - player, alive, dead, &lastteam, oneteam); - arenacount(player1, alive, dead, &lastteam, oneteam); - if (dead > 0 && (alive <= 1 || (m_teammode && oneteam))) { - conoutf( - @"arena round is over! next round in 5 seconds..."); - if (alive) - conoutf( - @"team %s is last man standing", lastteam); - else - conoutf(@"everyone died!"); - arenarespawnwait = lastmillis + 5000; - arenadetectwait = lastmillis + 10000; - player1.roll = 0; - } - } -} - -extern int democlientnum; - -void -otherplayers() -{ - [players enumerateObjectsUsingBlock:^(id player, size_t i, bool *stop) { - if (player == [OFNull null]) - return; - - const int lagtime = lastmillis - [player lastupdate]; - if (lagtime > 1000 && [player state] == CS_ALIVE) { - [player setState:CS_LAGGED]; - return; - } - - if (lagtime && [player state] != CS_DEAD && - (!demoplayback || i != democlientnum)) - // use physics to extrapolate player position - moveplayer(player, 2, false); - }]; -} - -void -respawn() -{ - if (player1.state == CS_DEAD) { - player1.attacking = false; - if (m_arena) { - conoutf(@"waiting for new round to start..."); - return; - } - if (m_sp) { - nextmode = gamemode; - changemap(clientmap); - return; - } // if we die in SP we try the same map again - respawnself(); - } -} - -int sleepwait = 0; -static OFString *sleepcmd = nil; -void -sleepf(OFString *msec, OFString *cmd) -{ - sleepwait = msec.cube_intValue + lastmillis; - sleepcmd = cmd; -} -COMMANDN(sleep, sleepf, ARG_2STR) - -void -updateworld(int millis) // main game update loop -{ - if (lastmillis) { - curtime = millis - lastmillis; - if (sleepwait && lastmillis > sleepwait) { - sleepwait = 0; - execute(sleepcmd, true); - } - physicsframe(); - checkquad(curtime); - if (m_arena) - arenarespawn(); - moveprojectiles((float)curtime); - demoplaybackstep(); - if (!demoplayback) { - if (getclientnum() >= 0) - // only shoot when connected to server - shoot(player1, &worldpos); - // do this first, so we have most accurate information - // when our player moves - gets2c(); - } - otherplayers(); - if (!demoplayback) { - monsterthink(); - if (player1.state == CS_DEAD) { - if (lastmillis - player1.lastaction < 2000) { - player1.move = player1.strafe = 0; - moveplayer(player1, 10, false); - } else if (!m_arena && !m_sp && - lastmillis - player1.lastaction > 10000) - respawn(); - } else if (!intermission) { - moveplayer(player1, 20, true); - checkitems(); - } - // do this last, to reduce the effective frame lag - c2sinfo(player1); - } - } - lastmillis = millis; -} - -// brute force but effective way to find a free spawn spot in the map -void -entinmap(DynamicEntity *d) -{ - loopi(100) // try max 100 times - { - float dx = (rnd(21) - 10) / 10.0f * i; // increasing distance - float dy = (rnd(21) - 10) / 10.0f * i; - OFVector3D old = d.o; - d.o = OFMakeVector3D(d.o.x + dx, d.o.y + dy, d.o.z); - if (collide(d, true, 0, 0)) - return; - d.o = old; - } - conoutf( - @"can't find entity spawn spot! (%d, %d)", (int)d.o.x, (int)d.o.y); - // leave ent at original pos, possibly stuck -} - -int spawncycle = -1; -int fixspawn = 2; - -// place at random spawn. also used by monsters! -void -spawnplayer(DynamicEntity *d) -{ - int r = fixspawn-- > 0 ? 4 : rnd(10) + 1; - loopi(r) spawncycle = findentity(PLAYERSTART, spawncycle + 1); - if (spawncycle != -1) { - d.o = OFMakeVector3D( - ents[spawncycle].x, ents[spawncycle].y, ents[spawncycle].z); - d.yaw = ents[spawncycle].attr1; - d.pitch = 0; - d.roll = 0; - } else - d.o = OFMakeVector3D((float)ssize / 2, (float)ssize / 2, 4); - entinmap(d); - spawnstate(d); - d.state = CS_ALIVE; -} - -// movement input code - -#define dir(name, v, d, s, os) \ - void name(bool isdown) \ - { \ - player1.s = isdown; \ - player1.v = isdown ? d : (player1.os ? -(d) : 0); \ - player1.lastmove = lastmillis; \ - } - -dir(backward, move, -1, k_down, k_up); -dir(forward, move, 1, k_up, k_down); -dir(left, strafe, 1, k_left, k_right); -dir(right, strafe, -1, k_right, k_left); - -void -attack(bool on) -{ - if (intermission) - return; - if (editmode) - editdrag(on); - else if ((player1.attacking = on)) - respawn(); -} - -void -jumpn(bool on) -{ - if (!intermission && (player1.jumpnext = on)) - respawn(); -} - -COMMAND(backward, ARG_DOWN) -COMMAND(forward, ARG_DOWN) -COMMAND(left, ARG_DOWN) -COMMAND(right, ARG_DOWN) -COMMANDN(jump, jumpn, ARG_DOWN) -COMMAND(attack, ARG_DOWN) -COMMAND(showscores, ARG_DOWN) - -void -fixplayer1range() -{ - const float MAXPITCH = 90.0f; - if (player1.pitch > MAXPITCH) - player1.pitch = MAXPITCH; - if (player1.pitch < -MAXPITCH) - player1.pitch = -MAXPITCH; - while (player1.yaw < 0.0f) - player1.yaw += 360.0f; - while (player1.yaw >= 360.0f) - player1.yaw -= 360.0f; -} - -void -mousemove(int dx, int dy) -{ - if (player1.state == CS_DEAD || intermission) - return; - const float SENSF = 33.0f; // try match quake sens - player1.yaw += (dx / SENSF) * (sensitivity / (float)sensitivityscale); - player1.pitch -= (dy / SENSF) * - (sensitivity / (float)sensitivityscale) * (invmouse ? -1 : 1); - fixplayer1range(); -} - -// damage arriving from the network, monsters, yourself, all ends up here. - -void -selfdamage(int damage, int actor, DynamicEntity *act) -{ - if (player1.state != CS_ALIVE || editmode || intermission) - return; - damageblend(damage); - demoblend(damage); - // let armour absorb when possible - int ad = damage * (player1.armourtype + 1) * 20 / 100; - if (ad > player1.armour) - ad = player1.armour; - player1.armour -= ad; - damage -= ad; - float droll = damage / 0.5f; - player1.roll += player1.roll > 0 - ? droll - : (player1.roll < 0 - ? -droll - : (rnd(2) ? droll - : -droll)); // give player a kick depending - // on amount of damage - if ((player1.health -= damage) <= 0) { - if (actor == -2) { - conoutf(@"you got killed by %@!", act.name); - } else if (actor == -1) { - actor = getclientnum(); - conoutf(@"you suicided!"); - addmsg(1, 2, SV_FRAGS, --player1.frags); - } else { - DynamicEntity *a = getclient(actor); - if (a != nil) { - if (isteam(a.team, player1.team)) - conoutf(@"you got fragged by a " - @"teammate (%@)", - a.name); - else - conoutf( - @"you got fragged by %@", a.name); - } - } - showscores(true); - addmsg(1, 2, SV_DIED, actor); - player1.lifesequence++; - player1.attacking = false; - player1.state = CS_DEAD; - player1.pitch = 0; - player1.roll = 60; - playsound(S_DIE1 + rnd(2), NULL); - spawnstate(player1); - player1.lastaction = lastmillis; - } else - playsound(S_PAIN6, NULL); -} - -void -timeupdate(int timeremain) -{ - if (!timeremain) { - intermission = true; - player1.attacking = false; - conoutf(@"intermission:"); - conoutf(@"game has ended!"); - showscores(true); - } else { - conoutf(@"time remaining: %d minutes", timeremain); - } -} - -DynamicEntity * -getclient(int cn) // ensure valid entity -{ - if (cn < 0 || cn >= MAXCLIENTS) { - neterr(@"clientnum"); - return nil; - } - while (cn >= players.count) - [players addObject:[OFNull null]]; - return (players[cn] != [OFNull null] ? players[cn] - : (players[cn] = newdynent())); -} - -void -setclient(int cn, id client) -{ - if (cn < 0 || cn >= MAXCLIENTS) - neterr(@"clientnum"); - while (cn >= players.count) - [players addObject:[OFNull null]]; - players[cn] = client; -} - -void -initclient() -{ - clientmap = @""; - initclientnet(); -} - -void -startmap(OFString *name) // called just after a map load -{ - if (netmapstart() && m_sp) { - gamemode = 0; - conoutf(@"coop sp not supported yet"); - } - sleepwait = 0; - monsterclear(); - projreset(); - spawncycle = -1; - spawnplayer(player1); - player1.frags = 0; - for (id player in players) - if (player != [OFNull null]) - [player setFrags:0]; - resetspawns(); - clientmap = name; - if (editmode) - toggleedit(); - setvar(@"gamespeed", 100); - setvar(@"fog", 180); - setvar(@"fogcolour", 0x8099B3); - showscores(false); - intermission = false; - Cube.sharedInstance.framesInMap = 0; - conoutf(@"game mode is %@", modestr(gamemode)); -} - -COMMANDN(map, changemap, ARG_1STR) ADDED src/clients.m Index: src/clients.m ================================================================== --- /dev/null +++ src/clients.m @@ -0,0 +1,412 @@ +// client.cpp, mostly network related client game code + +#include "cube.h" + +#import "DynamicEntity.h" + +static ENetHost *clienthost = NULL; +static int connecting = 0; +static int connattempts = 0; +static int disconnecting = 0; +// our client id in the game +int clientnum = -1; +// whether we need to tell the other clients our stats +bool c2sinit = false; + +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; + + if (name.length > 16) + name = [name substringToIndex:16]; + + player1.name = name; +} +COMMANDN(name, newname, ARG_1STR) + +void +newteam(OFString *name) +{ + c2sinit = false; + + if (name.length > 5) + name = [name substringToIndex:5]; + + player1.team = name; +} +COMMANDN(team, newteam, ARG_1STR) + +void +writeclientinfo(OFStream *stream) +{ + [stream writeFormat:@"name \"%@\"\nteam \"%@\"\n", player1.name, + player1.team]; +} + +void +connects(OFString *servername) +{ + disconnect(true, false); // reset state + addserver(servername); + + conoutf(@"attempting to connect to %@", servername); + ENetAddress address = { ENET_HOST_ANY, CUBE_SERVER_PORT }; + 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(false, false); + } +} + +void +disconnect(bool onlyclean, bool 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; + [players removeAllObjects]; + + localdisconnect(); + + if (!onlyclean) { + stop(); + localconnect(); + } +} + +void +trydisconnect() +{ + if (!clienthost) { + conoutf(@"not connected"); + return; + } + if (connecting) { + conoutf(@"aborting connection attempt"); + disconnect(false, false); + return; + } + conoutf(@"attempting to disconnect..."); + disconnect(0, !disconnecting); +} + +static OFString *ctext; +void +toserver(OFString *text) +{ + conoutf(@"%@:\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 *messages; + +void +addmsg(int rel, int num, int type, ...) +{ + if (demoplayback) + return; + if (num != msgsizelookup(type)) + fatal(@"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(false, false); +} + +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); +} + +// send update to the server +void +c2sinfo(DynamicEntity *d) +{ + 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; + // suggest server to change map + if (toservermap.length > 0) { + // 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); + // quantize coordinates to 1/16th of a cube, between 1 and 3 + // bytes + putint(&p, (int)(d.o.x * DMF)); + 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)); + // quantize to 1/100, almost always 1 byte + putint(&p, (int)(d.vel.x * DVF)); + 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 = @""; + } + // tell other clients who I am + if (!c2sinit) { + 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(false, false); + 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(false, false); + else + server_err(); + return; + } +} DELETED src/clients.mm Index: src/clients.mm ================================================================== --- src/clients.mm +++ /dev/null @@ -1,414 +0,0 @@ -// client.cpp, mostly network related client game code - -#include "cube.h" - -#import "DynamicEntity.h" - -static ENetHost *clienthost = NULL; -static int connecting = 0; -static int connattempts = 0; -static int disconnecting = 0; -// our client id in the game -int clientnum = -1; -// whether we need to tell the other clients our stats -bool c2sinit = false; - -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; - - if (name.length > 16) - name = [name substringToIndex:16]; - - player1.name = name; -} -COMMANDN(name, newname, ARG_1STR) - -void -newteam(OFString *name) -{ - c2sinit = false; - - if (name.length > 5) - name = [name substringToIndex:5]; - - player1.team = name; -} -COMMANDN(team, newteam, ARG_1STR) - -void -writeclientinfo(OFStream *stream) -{ - [stream writeFormat:@"name \"%@\"\nteam \"%@\"\n", player1.name, - player1.team]; -} - -void -connects(OFString *servername) -{ - disconnect(true, false); // reset state - addserver(servername); - - conoutf(@"attempting to connect to %@", servername); - ENetAddress address = { ENET_HOST_ANY, CUBE_SERVER_PORT }; - 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(false, false); - } -} - -void -disconnect(bool onlyclean, bool 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; - [players removeAllObjects]; - - localdisconnect(); - - if (!onlyclean) { - stop(); - localconnect(); - } -} - -void -trydisconnect() -{ - if (!clienthost) { - conoutf(@"not connected"); - return; - } - if (connecting) { - conoutf(@"aborting connection attempt"); - disconnect(false, false); - return; - } - conoutf(@"attempting to disconnect..."); - disconnect(0, !disconnecting); -} - -static OFString *ctext; -void -toserver(OFString *text) -{ - conoutf(@"%@:\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 *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(false, false); -} - -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); -} - -// send update to the server -void -c2sinfo(DynamicEntity *d) -{ - 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; - // suggest server to change map - if (toservermap.length > 0) { - // 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); - // quantize coordinates to 1/16th of a cube, between 1 and 3 - // bytes - putint(&p, (int)(d.o.x * DMF)); - 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)); - // quantize to 1/100, almost always 1 byte - putint(&p, (int)(d.vel.x * DVF)); - 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 = @""; - } - // tell other clients who I am - if (!c2sinit) { - 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(false, false); - 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(false, false); - else - server_err(); - return; - } -} ADDED src/clients2c.m Index: src/clients2c.m ================================================================== --- /dev/null +++ src/clients2c.m @@ -0,0 +1,409 @@ +// client processing of the incoming network stream + +#include "cube.h" + +#import "DynamicEntity.h" +#import "Entity.h" + +extern int clientnum; +extern bool c2sinit, senditemstoserver; +extern OFString *toservermap; +extern OFString *clientpassword; + +void +neterr(OFString *s) +{ + conoutf(@"illegal network message (%@)", s); + disconnect(false, false); +} + +void +changemapserv(OFString *name, int mode) // forced map change from the server +{ + gamemode = mode; + load_world(name); +} + +void +changemap(OFString *name) // request map change, server may ignore +{ + toservermap = name; +} + +// update the position of other clients in the game in our world +// don't care if he's in the scenery or other players, +// just don't overlap with our client + +void +updatepos(DynamicEntity *d) +{ + const float r = player1.radius + d.radius; + const float dx = player1.o.x - d.o.x; + const float dy = player1.o.y - d.o.y; + const float dz = player1.o.z - d.o.z; + const float rz = player1.aboveeye + d.eyeheight; + const float fx = (float)fabs(dx), fy = (float)fabs(dy), + fz = (float)fabs(dz); + if (fx < r && fy < r && fz < rz && d.state != CS_DEAD) { + if (fx < fy) + // push aside + d.o = OFMakeVector3D(d.o.x, + d.o.y + (dy < 0 ? r - fy : -(r - fy)), d.o.z); + else + d.o = OFMakeVector3D( + d.o.x + (dx < 0 ? r - fx : -(r - fx)), d.o.y, + d.o.z); + } + int lagtime = lastmillis - d.lastupdate; + if (lagtime) { + d.plag = (d.plag * 5 + lagtime) / 6; + d.lastupdate = lastmillis; + } +} + +// processes any updates from the server +void +localservertoclient(uchar *buf, int len) +{ + if (ENET_NET_TO_HOST_16(*(ushort *)buf) != len) + neterr(@"packet length"); + incomingdemodata(buf, len, false); + + uchar *end = buf + len; + uchar *p = buf + 2; + char text[MAXTRANS]; + int cn = -1, type; + DynamicEntity *d = nil; + bool mapchanged = false; + + while (p < end) + switch (type = getint(&p)) { + case SV_INITS2C: // welcome messsage from the server + { + cn = getint(&p); + int prot = getint(&p); + if (prot != PROTOCOL_VERSION) { + conoutf(@"you are using a different game " + @"protocol (you: %d, server: %d)", + PROTOCOL_VERSION, prot); + disconnect(false, false); + return; + } + toservermap = @""; + clientnum = cn; // we are now fully connected + if (!getint(&p)) + // we are the first client on this server, set + // map + toservermap = getclientmap(); + sgetstr(); + if (text[0] && + strcmp(text, clientpassword.UTF8String)) { + conoutf(@"you need to set the correct password " + @"to join this server!"); + disconnect(false, false); + return; + } + if (getint(&p) == 1) + conoutf(@"server is FULL, disconnecting.."); + break; + } + + case SV_POS: { + // position of another client + cn = getint(&p); + d = getclient(cn); + if (d == nil) + return; + OFVector3D tmp; + tmp.x = getint(&p) / DMF; + tmp.y = getint(&p) / DMF; + tmp.z = getint(&p) / DMF; + d.o = tmp; + d.yaw = getint(&p) / DAF; + d.pitch = getint(&p) / DAF; + d.roll = getint(&p) / DAF; + tmp.x = getint(&p) / DVF; + tmp.y = getint(&p) / DVF; + tmp.z = getint(&p) / DVF; + d.vel = tmp; + int f = getint(&p); + d.strafe = (f & 3) == 3 ? -1 : f & 3; + f >>= 2; + d.move = (f & 3) == 3 ? -1 : f & 3; + d.onfloor = (f >> 2) & 1; + int state = f >> 3; + if (state == CS_DEAD && d.state != CS_DEAD) + d.lastaction = lastmillis; + d.state = state; + if (!demoplayback) + updatepos(d); + break; + } + + case SV_SOUND: { + OFVector3D loc = d.o; + playsound(getint(&p), &loc); + break; + } + + case SV_TEXT: + sgetstr(); + conoutf(@"%@:\f %s", d.name, text); + break; + + case SV_MAPCHANGE: + sgetstr(); + changemapserv(@(text), getint(&p)); + mapchanged = true; + break; + + case SV_ITEMLIST: { + int n; + if (mapchanged) { + senditemstoserver = false; + resetspawns(); + } + while ((n = getint(&p)) != -1) + if (mapchanged) + setspawn(n, true); + break; + } + // server requests next map + case SV_MAPRELOAD: { + getint(&p); + OFString *nextmapalias = [OFString + stringWithFormat:@"nextmap_%@", getclientmap()]; + OFString *map = + getalias(nextmapalias); // look up map in the cycle + changemap(map != nil ? map : getclientmap()); + break; + } + + // another client either connected or changed name/team + case SV_INITC2S: { + sgetstr(); + if (d.name.length > 0) { + // already connected + if (![d.name isEqual:@(text)]) + conoutf(@"%@ is now known as %s", + d.name, text); + } else { + // new client + + // send new players my info again + c2sinit = false; + conoutf(@"connected: %s", text); + } + d.name = @(text); + sgetstr(); + d.team = @(text); + d.lifesequence = getint(&p); + break; + } + + case SV_CDIS: + cn = getint(&p); + if ((d = getclient(cn)) == nil) + break; + conoutf(@"player %@ disconnected", + d.name.length ? d.name : @"[incompatible client]"); + players[cn] = [OFNull null]; + break; + + case SV_SHOT: { + int gun = getint(&p); + OFVector3D s, e; + s.x = getint(&p) / DMF; + s.y = getint(&p) / DMF; + s.z = getint(&p) / DMF; + e.x = getint(&p) / DMF; + e.y = getint(&p) / DMF; + e.z = getint(&p) / DMF; + if (gun == GUN_SG) + createrays(&s, &e); + shootv(gun, &s, &e, d, false); + break; + } + + case SV_DAMAGE: { + int target = getint(&p); + int damage = getint(&p); + int ls = getint(&p); + if (target == clientnum) { + if (ls == player1.lifesequence) + selfdamage(damage, cn, d); + } else { + OFVector3D loc = getclient(target).o; + playsound(S_PAIN1 + rnd(5), &loc); + } + break; + } + + case SV_DIED: { + int actor = getint(&p); + if (actor == cn) { + conoutf(@"%@ suicided", d.name); + } else if (actor == clientnum) { + int frags; + if (isteam(player1.team, d.team)) { + frags = -1; + conoutf(@"you fragged a teammate (%@)", + d.name); + } else { + frags = 1; + conoutf(@"you fragged %@", d.name); + } + addmsg( + 1, 2, SV_FRAGS, (player1.frags += frags)); + } else { + DynamicEntity *a = getclient(actor); + if (a != nil) { + if (isteam(a.team, d.name)) + conoutf(@"%@ fragged his " + @"teammate (%@)", + a.name, d.name); + else + conoutf(@"%@ fragged %@", + a.name, d.name); + } + } + OFVector3D loc = d.o; + playsound(S_DIE1 + rnd(2), &loc); + d.lifesequence++; + break; + } + + case SV_FRAGS: + [players[cn] setFrags:getint(&p)]; + break; + + case SV_ITEMPICKUP: + setspawn(getint(&p), false); + getint(&p); + break; + + case SV_ITEMSPAWN: { + uint i = getint(&p); + setspawn(i, true); + if (i >= (uint)ents.count) + break; + OFVector3D v = + OFMakeVector3D(ents[i].x, ents[i].y, ents[i].z); + playsound(S_ITEMSPAWN, &v); + break; + } + // server acknowledges that I picked up this item + case SV_ITEMACC: + realpickup(getint(&p), player1); + break; + + case SV_EDITH: // coop editing messages, should be extended to + // include all possible editing ops + case SV_EDITT: + case SV_EDITS: + case SV_EDITD: + case SV_EDITE: { + int x = getint(&p); + int y = getint(&p); + int xs = getint(&p); + int ys = getint(&p); + int v = getint(&p); + struct block b = { x, y, xs, ys }; + switch (type) { + case SV_EDITH: + editheightxy(v != 0, getint(&p), &b); + break; + case SV_EDITT: + edittexxy(v, getint(&p), &b); + break; + case SV_EDITS: + edittypexy(v, &b); + break; + case SV_EDITD: + setvdeltaxy(v, &b); + break; + case SV_EDITE: + editequalisexy((v != 0), &b); + break; + } + break; + } + + case SV_EDITENT: // coop edit of ent + { + uint i = getint(&p); + + while ((uint)ents.count <= i) { + Entity *e = [Entity entity]; + e.type = NOTUSED; + [ents addObject:e]; + } + + int to = ents[i].type; + ents[i].type = getint(&p); + ents[i].x = getint(&p); + ents[i].y = getint(&p); + ents[i].z = getint(&p); + ents[i].attr1 = getint(&p); + ents[i].attr2 = getint(&p); + ents[i].attr3 = getint(&p); + ents[i].attr4 = getint(&p); + ents[i].spawned = false; + if (ents[i].type == LIGHT || to == LIGHT) + calclight(); + break; + } + + case SV_PING: + getint(&p); + break; + + case SV_PONG: + addmsg(0, 2, SV_CLIENTPING, + player1.ping = + (player1.ping * 5 + lastmillis - getint(&p)) / + 6); + break; + + case SV_CLIENTPING: + [players[cn] setPing:getint(&p)]; + break; + + case SV_GAMEMODE: + nextmode = getint(&p); + break; + + case SV_TIMEUP: + timeupdate(getint(&p)); + break; + + case SV_RECVMAP: { + sgetstr(); + conoutf(@"received map \"%s\" from server, reloading..", + text); + int mapsize = getint(&p); + OFString *string = @(text); + writemap(string, mapsize, p); + p += mapsize; + changemapserv(string, gamemode); + break; + } + + case SV_SERVMSG: + sgetstr(); + conoutf(@"%s", text); + break; + + case SV_EXT: // so we can messages without breaking previous + // clients/servers, if necessary + { + for (int n = getint(&p); n; n--) + getint(&p); + break; + } + + default: + neterr(@"type"); + return; + } +} DELETED src/clients2c.mm Index: src/clients2c.mm ================================================================== --- src/clients2c.mm +++ /dev/null @@ -1,409 +0,0 @@ -// client processing of the incoming network stream - -#include "cube.h" - -#import "DynamicEntity.h" -#import "Entity.h" - -extern int clientnum; -extern bool c2sinit, senditemstoserver; -extern OFString *toservermap; -extern OFString *clientpassword; - -void -neterr(OFString *s) -{ - conoutf(@"illegal network message (%@)", s); - disconnect(false, false); -} - -void -changemapserv(OFString *name, int mode) // forced map change from the server -{ - gamemode = mode; - load_world(name); -} - -void -changemap(OFString *name) // request map change, server may ignore -{ - toservermap = name; -} - -// update the position of other clients in the game in our world -// don't care if he's in the scenery or other players, -// just don't overlap with our client - -void -updatepos(DynamicEntity *d) -{ - const float r = player1.radius + d.radius; - const float dx = player1.o.x - d.o.x; - const float dy = player1.o.y - d.o.y; - const float dz = player1.o.z - d.o.z; - const float rz = player1.aboveeye + d.eyeheight; - const float fx = (float)fabs(dx), fy = (float)fabs(dy), - fz = (float)fabs(dz); - if (fx < r && fy < r && fz < rz && d.state != CS_DEAD) { - if (fx < fy) - // push aside - d.o = OFMakeVector3D(d.o.x, - d.o.y + (dy < 0 ? r - fy : -(r - fy)), d.o.z); - else - d.o = OFMakeVector3D( - d.o.x + (dx < 0 ? r - fx : -(r - fx)), d.o.y, - d.o.z); - } - int lagtime = lastmillis - d.lastupdate; - if (lagtime) { - d.plag = (d.plag * 5 + lagtime) / 6; - d.lastupdate = lastmillis; - } -} - -// processes any updates from the server -void -localservertoclient(uchar *buf, int len) -{ - if (ENET_NET_TO_HOST_16(*(ushort *)buf) != len) - neterr(@"packet length"); - incomingdemodata(buf, len, false); - - uchar *end = buf + len; - uchar *p = buf + 2; - char text[MAXTRANS]; - int cn = -1, type; - DynamicEntity *d = nil; - bool mapchanged = false; - - while (p < end) - switch (type = getint(&p)) { - case SV_INITS2C: // welcome messsage from the server - { - cn = getint(&p); - int prot = getint(&p); - if (prot != PROTOCOL_VERSION) { - conoutf(@"you are using a different game " - @"protocol (you: %d, server: %d)", - PROTOCOL_VERSION, prot); - disconnect(false, false); - return; - } - toservermap = @""; - clientnum = cn; // we are now fully connected - if (!getint(&p)) - // we are the first client on this server, set - // map - toservermap = getclientmap(); - sgetstr(); - if (text[0] && - strcmp(text, clientpassword.UTF8String)) { - conoutf(@"you need to set the correct password " - @"to join this server!"); - disconnect(false, false); - return; - } - if (getint(&p) == 1) - conoutf(@"server is FULL, disconnecting.."); - break; - } - - case SV_POS: { - // position of another client - cn = getint(&p); - d = getclient(cn); - if (d == nil) - return; - OFVector3D tmp; - tmp.x = getint(&p) / DMF; - tmp.y = getint(&p) / DMF; - tmp.z = getint(&p) / DMF; - d.o = tmp; - d.yaw = getint(&p) / DAF; - d.pitch = getint(&p) / DAF; - d.roll = getint(&p) / DAF; - tmp.x = getint(&p) / DVF; - tmp.y = getint(&p) / DVF; - tmp.z = getint(&p) / DVF; - d.vel = tmp; - int f = getint(&p); - d.strafe = (f & 3) == 3 ? -1 : f & 3; - f >>= 2; - d.move = (f & 3) == 3 ? -1 : f & 3; - d.onfloor = (f >> 2) & 1; - int state = f >> 3; - if (state == CS_DEAD && d.state != CS_DEAD) - d.lastaction = lastmillis; - d.state = state; - if (!demoplayback) - updatepos(d); - break; - } - - case SV_SOUND: { - OFVector3D loc = d.o; - playsound(getint(&p), &loc); - break; - } - - case SV_TEXT: - sgetstr(); - conoutf(@"%@:\f %s", d.name, text); - break; - - case SV_MAPCHANGE: - sgetstr(); - changemapserv(@(text), getint(&p)); - mapchanged = true; - break; - - case SV_ITEMLIST: { - int n; - if (mapchanged) { - senditemstoserver = false; - resetspawns(); - } - while ((n = getint(&p)) != -1) - if (mapchanged) - setspawn(n, true); - break; - } - // server requests next map - case SV_MAPRELOAD: { - getint(&p); - OFString *nextmapalias = [OFString - stringWithFormat:@"nextmap_%@", getclientmap()]; - OFString *map = - getalias(nextmapalias); // look up map in the cycle - changemap(map != nil ? map : getclientmap()); - break; - } - - // another client either connected or changed name/team - case SV_INITC2S: { - sgetstr(); - if (d.name.length > 0) { - // already connected - if (![d.name isEqual:@(text)]) - conoutf(@"%@ is now known as %s", - d.name, text); - } else { - // new client - - // send new players my info again - c2sinit = false; - conoutf(@"connected: %s", text); - } - d.name = @(text); - sgetstr(); - d.team = @(text); - d.lifesequence = getint(&p); - break; - } - - case SV_CDIS: - cn = getint(&p); - if ((d = getclient(cn)) == nil) - break; - conoutf(@"player %@ disconnected", - d.name.length ? d.name : @"[incompatible client]"); - players[cn] = [OFNull null]; - break; - - case SV_SHOT: { - int gun = getint(&p); - OFVector3D s, e; - s.x = getint(&p) / DMF; - s.y = getint(&p) / DMF; - s.z = getint(&p) / DMF; - e.x = getint(&p) / DMF; - e.y = getint(&p) / DMF; - e.z = getint(&p) / DMF; - if (gun == GUN_SG) - createrays(&s, &e); - shootv(gun, &s, &e, d, false); - break; - } - - case SV_DAMAGE: { - int target = getint(&p); - int damage = getint(&p); - int ls = getint(&p); - if (target == clientnum) { - if (ls == player1.lifesequence) - selfdamage(damage, cn, d); - } else { - OFVector3D loc = getclient(target).o; - playsound(S_PAIN1 + rnd(5), &loc); - } - break; - } - - case SV_DIED: { - int actor = getint(&p); - if (actor == cn) { - conoutf(@"%@ suicided", d.name); - } else if (actor == clientnum) { - int frags; - if (isteam(player1.team, d.team)) { - frags = -1; - conoutf(@"you fragged a teammate (%@)", - d.name); - } else { - frags = 1; - conoutf(@"you fragged %@", d.name); - } - addmsg( - 1, 2, SV_FRAGS, (player1.frags += frags)); - } else { - DynamicEntity *a = getclient(actor); - if (a != nil) { - if (isteam(a.team, d.name)) - conoutf(@"%@ fragged his " - @"teammate (%@)", - a.name, d.name); - else - conoutf(@"%@ fragged %@", - a.name, d.name); - } - } - OFVector3D loc = d.o; - playsound(S_DIE1 + rnd(2), &loc); - d.lifesequence++; - break; - } - - case SV_FRAGS: - [players[cn] setFrags:getint(&p)]; - break; - - case SV_ITEMPICKUP: - setspawn(getint(&p), false); - getint(&p); - break; - - case SV_ITEMSPAWN: { - uint i = getint(&p); - setspawn(i, true); - if (i >= (uint)ents.count) - break; - OFVector3D v = - OFMakeVector3D(ents[i].x, ents[i].y, ents[i].z); - playsound(S_ITEMSPAWN, &v); - break; - } - // server acknowledges that I picked up this item - case SV_ITEMACC: - realpickup(getint(&p), player1); - break; - - case SV_EDITH: // coop editing messages, should be extended to - // include all possible editing ops - case SV_EDITT: - case SV_EDITS: - case SV_EDITD: - case SV_EDITE: { - int x = getint(&p); - int y = getint(&p); - int xs = getint(&p); - int ys = getint(&p); - int v = getint(&p); - block b = { x, y, xs, ys }; - switch (type) { - case SV_EDITH: - editheightxy(v != 0, getint(&p), &b); - break; - case SV_EDITT: - edittexxy(v, getint(&p), &b); - break; - case SV_EDITS: - edittypexy(v, &b); - break; - case SV_EDITD: - setvdeltaxy(v, &b); - break; - case SV_EDITE: - editequalisexy((v != 0), &b); - break; - } - break; - } - - case SV_EDITENT: // coop edit of ent - { - uint i = getint(&p); - - while ((uint)ents.count <= i) { - Entity *e = [Entity entity]; - e.type = NOTUSED; - [ents addObject:e]; - } - - int to = ents[i].type; - ents[i].type = getint(&p); - ents[i].x = getint(&p); - ents[i].y = getint(&p); - ents[i].z = getint(&p); - ents[i].attr1 = getint(&p); - ents[i].attr2 = getint(&p); - ents[i].attr3 = getint(&p); - ents[i].attr4 = getint(&p); - ents[i].spawned = false; - if (ents[i].type == LIGHT || to == LIGHT) - calclight(); - break; - } - - case SV_PING: - getint(&p); - break; - - case SV_PONG: - addmsg(0, 2, SV_CLIENTPING, - player1.ping = - (player1.ping * 5 + lastmillis - getint(&p)) / - 6); - break; - - case SV_CLIENTPING: - [players[cn] setPing:getint(&p)]; - break; - - case SV_GAMEMODE: - nextmode = getint(&p); - break; - - case SV_TIMEUP: - timeupdate(getint(&p)); - break; - - case SV_RECVMAP: { - sgetstr(); - conoutf(@"received map \"%s\" from server, reloading..", - text); - int mapsize = getint(&p); - OFString *string = @(text); - writemap(string, mapsize, p); - p += mapsize; - changemapserv(string, gamemode); - break; - } - - case SV_SERVMSG: - sgetstr(); - conoutf(@"%s", text); - break; - - case SV_EXT: // so we can messages without breaking previous - // clients/servers, if necessary - { - for (int n = getint(&p); n; n--) - getint(&p); - break; - } - - default: - neterr(@"type"); - return; - } -} ADDED src/console.m Index: src/console.m ================================================================== --- /dev/null +++ src/console.m @@ -0,0 +1,293 @@ +// console.cpp: the console buffer, its display, and command line control + +#include "cube.h" + +#include + +#import "ConsoleLine.h" +#import "KeyMapping.h" +#import "OFString+Cube.h" + +static OFMutableArray *conlines; + +const int ndraw = 5; +const int WORDWRAP = 80; +int conskip = 0; + +bool saycommandon = false; +static OFMutableString *commandbuf; + +void +setconskip(int n) +{ + conskip += n; + if (conskip < 0) + conskip = 0; +} +COMMANDN(conskip, setconskip, ARG_1INT) + +static void +conline(OFString *sf, bool highlight) // add a line to the console buffer +{ + OFMutableString *text; + + // constrain the buffer size + if (conlines.count > 100) { + text = [conlines.lastObject.text mutableCopy]; + [conlines removeLastObject]; + } else + text = [OFMutableString string]; + + if (highlight) + // show line in a different colour, for chat etc. + [text appendString:@"\f"]; + + [text appendString:sf]; + + if (conlines == nil) + conlines = [[OFMutableArray alloc] init]; + + [conlines insertObject:[ConsoleLine lineWithText:text + outtime:lastmillis] + atIndex:0]; + + puts(text.UTF8String); +#ifndef OF_WINDOWS + fflush(stdout); +#endif +} + +void +conoutf(OFConstantString *format, ...) +{ + va_list arguments; + va_start(arguments, format); + + OFString *string = [[OFString alloc] initWithFormat:format + arguments:arguments]; + + va_end(arguments); + + int n = 0; + while (string.length > WORDWRAP) { + conline([string substringToIndex:WORDWRAP], n++ != 0); + string = [string substringFromIndex:WORDWRAP]; + } + conline(string, n != 0); +} + +// render buffer taking into account time & scrolling +void +renderconsole() +{ + int nd = 0; + OFString *refs[ndraw]; + + size_t i = 0; + for (ConsoleLine *conline in conlines) { + if (conskip ? i >= conskip - 1 || i >= conlines.count - ndraw + : lastmillis - conline.outtime < 20000) { + refs[nd++] = conline.text; + if (nd == ndraw) + break; + } + + i++; + } + + loopj(nd) + { + draw_text(refs[j], FONTH / 3, + (FONTH / 4 * 5) * (nd - j - 1) + FONTH / 3, 2); + } +} + +// keymap is defined externally in keymap.cfg + +static OFMutableArray *keyMappings = nil; + +void +keymap(OFString *code, OFString *key, OFString *action) +{ + if (keyMappings == nil) + keyMappings = [[OFMutableArray alloc] init]; + + KeyMapping *mapping = [KeyMapping mappingWithCode:code.cube_intValue + name:key]; + mapping.action = action; + [keyMappings addObject:mapping]; +} +COMMAND(keymap, ARG_3STR) + +void +bindkey(OFString *key, OFString *action) +{ + for (KeyMapping *mapping in keyMappings) { + if ([mapping.name caseInsensitiveCompare:key] == + OFOrderedSame) { + mapping.action = action; + return; + } + } + + conoutf(@"unknown key \"%@\"", key); +} +COMMANDN(bind, bindkey, ARG_2STR) + +void +saycommand(OFString *init) // turns input to the command line on or off +{ + saycommandon = (init != nil); + if (saycommandon) + SDL_StartTextInput(); + else + SDL_StopTextInput(); + + if (!editmode) + Cube.sharedInstance.repeatsKeys = saycommandon; + + if (init == nil) + init = @""; + + commandbuf = [init mutableCopy]; +} +COMMAND(saycommand, ARG_VARI) + +void +mapmsg(OFString *s) +{ + memset(hdr.maptitle, '\0', sizeof(hdr.maptitle)); + strncpy(hdr.maptitle, s.UTF8String, 127); +} +COMMAND(mapmsg, ARG_1STR) + +void +pasteconsole() +{ + [commandbuf appendString:@(SDL_GetClipboardText())]; +} + +static OFMutableArray *vhistory; +static int histpos = 0; + +void +history(int n) +{ + static bool rec = false; + + if (!rec && n >= 0 && n < vhistory.count) { + rec = true; + execute(vhistory[vhistory.count - n - 1], true); + rec = false; + } +} +COMMAND(history, ARG_1INT) + +void +keypress(int code, bool isDown) +{ + // keystrokes go to commandline + if (saycommandon) { + if (isDown) { + switch (code) { + case SDLK_RETURN: + break; + + case SDLK_BACKSPACE: + case SDLK_LEFT: + if (commandbuf.length > 0) + [commandbuf + deleteCharactersInRange: + OFMakeRange( + commandbuf.length - 1, 1)]; + + resetcomplete(); + break; + + case SDLK_UP: + if (histpos) + commandbuf = + [vhistory[--histpos] mutableCopy]; + break; + + case SDLK_DOWN: + if (histpos < vhistory.count) + commandbuf = + [vhistory[histpos++] mutableCopy]; + break; + + case SDLK_TAB: + complete(commandbuf); + break; + + case SDLK_v: + if (SDL_GetModState() & + (KMOD_LCTRL | KMOD_RCTRL)) { + pasteconsole(); + return; + } + + default: + resetcomplete(); + } + } else { + if (code == SDLK_RETURN) { + if (commandbuf.length > 0) { + if (vhistory == nil) + vhistory = + [[OFMutableArray alloc] + init]; + + if (vhistory.count == 0 || + ![vhistory.lastObject + isEqual:commandbuf]) { + // cap this? + [vhistory addObject:[commandbuf + copy]]; + } + histpos = vhistory.count; + if ([commandbuf hasPrefix:@"/"]) + execute(commandbuf, true); + else + toserver(commandbuf); + } + saycommand(NULL); + } else if (code == SDLK_ESCAPE) { + saycommand(NULL); + } + } + } else if (!menukey(code, isDown)) { + // keystrokes go to menu + + for (KeyMapping *mapping in keyMappings) { + if (mapping.code == code) { + // keystrokes go to game, lookup in keymap and + // execute + execute(mapping.action, isDown); + return; + } + } + } +} + +void +input(OFString *text) +{ + if (saycommandon) + [commandbuf appendString:text]; +} + +OFString * +getcurcommand() +{ + return saycommandon ? commandbuf : NULL; +} + +void +writebinds(OFStream *stream) +{ + for (KeyMapping *mapping in keyMappings) + if (mapping.action.length > 0) + [stream writeFormat:@"bind \"%@\" [%@]\n", mapping.name, + mapping.action]; +} DELETED src/console.mm Index: src/console.mm ================================================================== --- src/console.mm +++ /dev/null @@ -1,293 +0,0 @@ -// console.cpp: the console buffer, its display, and command line control - -#include "cube.h" - -#include - -#import "ConsoleLine.h" -#import "KeyMapping.h" -#import "OFString+Cube.h" - -static OFMutableArray *conlines; - -const int ndraw = 5; -const int WORDWRAP = 80; -int conskip = 0; - -bool saycommandon = false; -static OFMutableString *commandbuf; - -void -setconskip(int n) -{ - conskip += n; - if (conskip < 0) - conskip = 0; -} -COMMANDN(conskip, setconskip, ARG_1INT) - -static void -conline(OFString *sf, bool highlight) // add a line to the console buffer -{ - OFMutableString *text; - - // constrain the buffer size - if (conlines.count > 100) { - text = [conlines.lastObject.text mutableCopy]; - [conlines removeLastObject]; - } else - text = [OFMutableString string]; - - if (highlight) - // show line in a different colour, for chat etc. - [text appendString:@"\f"]; - - [text appendString:sf]; - - if (conlines == nil) - conlines = [[OFMutableArray alloc] init]; - - [conlines insertObject:[ConsoleLine lineWithText:text - outtime:lastmillis] - atIndex:0]; - - puts(text.UTF8String); -#ifndef OF_WINDOWS - fflush(stdout); -#endif -} - -void -conoutf(OFConstantString *format, ...) -{ - va_list arguments; - va_start(arguments, format); - - OFString *string = [[OFString alloc] initWithFormat:format - arguments:arguments]; - - va_end(arguments); - - int n = 0; - while (string.length > WORDWRAP) { - conline([string substringToIndex:WORDWRAP], n++ != 0); - string = [string substringFromIndex:WORDWRAP]; - } - conline(string, n != 0); -} - -// render buffer taking into account time & scrolling -void -renderconsole() -{ - int nd = 0; - OFString *refs[ndraw]; - - size_t i = 0; - for (ConsoleLine *conline in conlines) { - if (conskip ? i >= conskip - 1 || i >= conlines.count - ndraw - : lastmillis - conline.outtime < 20000) { - refs[nd++] = conline.text; - if (nd == ndraw) - break; - } - - i++; - } - - loopj(nd) - { - draw_text(refs[j], FONTH / 3, - (FONTH / 4 * 5) * (nd - j - 1) + FONTH / 3, 2); - } -} - -// keymap is defined externally in keymap.cfg - -static OFMutableArray *keyMappings = nil; - -void -keymap(OFString *code, OFString *key, OFString *action) -{ - if (keyMappings == nil) - keyMappings = [[OFMutableArray alloc] init]; - - KeyMapping *mapping = [KeyMapping mappingWithCode:code.cube_intValue - name:key]; - mapping.action = action; - [keyMappings addObject:mapping]; -} -COMMAND(keymap, ARG_3STR) - -void -bindkey(OFString *key, OFString *action) -{ - for (KeyMapping *mapping in keyMappings) { - if ([mapping.name caseInsensitiveCompare:key] == - OFOrderedSame) { - mapping.action = action; - return; - } - } - - conoutf(@"unknown key \"%@\"", key); -} -COMMANDN(bind, bindkey, ARG_2STR) - -void -saycommand(OFString *init) // turns input to the command line on or off -{ - saycommandon = (init != nil); - if (saycommandon) - SDL_StartTextInput(); - else - SDL_StopTextInput(); - - if (!editmode) - Cube.sharedInstance.repeatsKeys = saycommandon; - - if (init == nil) - init = @""; - - commandbuf = [init mutableCopy]; -} -COMMAND(saycommand, ARG_VARI) - -void -mapmsg(OFString *s) -{ - memset(hdr.maptitle, '\0', sizeof(hdr.maptitle)); - strncpy(hdr.maptitle, s.UTF8String, 127); -} -COMMAND(mapmsg, ARG_1STR) - -void -pasteconsole() -{ - [commandbuf appendString:@(SDL_GetClipboardText())]; -} - -static OFMutableArray *vhistory; -static int histpos = 0; - -void -history(int n) -{ - static bool rec = false; - - if (!rec && n >= 0 && n < vhistory.count) { - rec = true; - execute(vhistory[vhistory.count - n - 1], true); - rec = false; - } -} -COMMAND(history, ARG_1INT) - -void -keypress(int code, bool isDown) -{ - // keystrokes go to commandline - if (saycommandon) { - if (isDown) { - switch (code) { - case SDLK_RETURN: - break; - - case SDLK_BACKSPACE: - case SDLK_LEFT: - if (commandbuf.length > 0) - [commandbuf - deleteCharactersInRange: - OFMakeRange( - commandbuf.length - 1, 1)]; - - resetcomplete(); - break; - - case SDLK_UP: - if (histpos) - commandbuf = - [vhistory[--histpos] mutableCopy]; - break; - - case SDLK_DOWN: - if (histpos < vhistory.count) - commandbuf = - [vhistory[histpos++] mutableCopy]; - break; - - case SDLK_TAB: - complete(commandbuf); - break; - - case SDLK_v: - if (SDL_GetModState() & - (KMOD_LCTRL | KMOD_RCTRL)) { - pasteconsole(); - return; - } - - default: - resetcomplete(); - } - } else { - if (code == SDLK_RETURN) { - if (commandbuf.length > 0) { - if (vhistory == nil) - vhistory = - [[OFMutableArray alloc] - init]; - - if (vhistory.count == 0 || - ![vhistory.lastObject - isEqual:commandbuf]) { - // cap this? - [vhistory addObject:[commandbuf - copy]]; - } - histpos = vhistory.count; - if ([commandbuf hasPrefix:@"/"]) - execute(commandbuf, true); - else - toserver(commandbuf); - } - saycommand(NULL); - } else if (code == SDLK_ESCAPE) { - saycommand(NULL); - } - } - } else if (!menukey(code, isDown)) { - // keystrokes go to menu - - for (KeyMapping *mapping in keyMappings) { - if (mapping.code == code) { - // keystrokes go to game, lookup in keymap and - // execute - execute(mapping.action, isDown); - return; - } - } - } -} - -void -input(OFString *text) -{ - if (saycommandon) - [commandbuf appendString:text]; -} - -OFString * -getcurcommand() -{ - return saycommandon ? commandbuf : NULL; -} - -void -writebinds(OFStream *stream) -{ - for (KeyMapping *mapping in keyMappings) - if (mapping.action.length > 0) - [stream writeFormat:@"bind \"%@\" [%@]\n", mapping.name, - mapping.action]; -} ADDED src/editing.m Index: src/editing.m ================================================================== --- /dev/null +++ src/editing.m @@ -0,0 +1,642 @@ +// editing.cpp: most map editing commands go here, entity editing commands are +// in world.cpp + +#include "cube.h" + +#import "DynamicEntity.h" +#import "OFString+Cube.h" + +bool editmode = false; + +// the current selection, used by almost all editing commands +// invariant: all code assumes that these are kept inside MINBORD distance of +// the edge of the map + +struct block sel; + +OF_CONSTRUCTOR() +{ + enqueueInit(^{ + sel = (struct block) { + variable(@"selx", 0, 0, 4096, &sel.x, NULL, false), + variable(@"sely", 0, 0, 4096, &sel.y, NULL, false), + variable(@"selxs", 0, 0, 4096, &sel.xs, NULL, false), + variable(@"selys", 0, 0, 4096, &sel.ys, NULL, false), + }; + }); +} + +int selh = 0; +bool selset = false; + +#define loopselxy(b) \ + { \ + makeundo(); \ + loop(x, sel->xs) loop(y, sel->ys) \ + { \ + struct sqr *s = S(sel->x + x, sel->y + y); \ + b; \ + } \ + remip(sel, 0); \ + } + +int cx, cy, ch; + +int curedittex[] = { -1, -1, -1 }; + +bool dragging = false; +int lastx, lasty, lasth; + +int lasttype = 0, lasttex = 0; +static struct sqr rtex; + +VAR(editing, 0, 0, 1); + +void +toggleedit() +{ + if (player1.state == CS_DEAD) + return; // do not allow dead players to edit to avoid state + // confusion + if (!editmode && !allowedittoggle()) + return; // not in most multiplayer modes + if (!(editmode = !editmode)) { + settagareas(); // reset triggers to allow quick playtesting + entinmap(player1); // find spawn closest to current floating pos + } else { + resettagareas(); // clear trigger areas to allow them to be + // edited + player1.health = 100; + if (m_classicsp) + monsterclear(); // all monsters back at their spawns for + // editing + projreset(); + } + Cube.sharedInstance.repeatsKeys = editmode; + selset = false; + editing = editmode; +} +COMMANDN(edittoggle, toggleedit, ARG_NONE) + +void +correctsel() // ensures above invariant +{ + selset = !OUTBORD(sel.x, sel.y); + int bsize = ssize - MINBORD; + if (sel.xs + sel.x > bsize) + sel.xs = bsize - sel.x; + if (sel.ys + sel.y > bsize) + sel.ys = bsize - sel.y; + if (sel.xs <= 0 || sel.ys <= 0) + selset = false; +} + +bool +noteditmode() +{ + correctsel(); + if (!editmode) + conoutf(@"this function is only allowed in edit mode"); + return !editmode; +} + +bool +noselection() +{ + if (!selset) + conoutf(@"no selection"); + return !selset; +} + +#define EDITSEL \ + if (noteditmode() || noselection()) \ + return; +#define EDITSELMP \ + if (noteditmode() || noselection() || multiplayer()) \ + return; +#define EDITMP \ + if (noteditmode() || multiplayer()) \ + return; + +void +selectpos(int x, int y, int xs, int ys) +{ + struct block s = { x, y, xs, ys }; + sel = s; + selh = 0; + correctsel(); +} + +void +makesel() +{ + struct block s = { min(lastx, cx), min(lasty, cy), abs(lastx - cx) + 1, + abs(lasty - cy) + 1 }; + sel = s; + selh = max(lasth, ch); + correctsel(); + if (selset) + rtex = *S(sel.x, sel.y); +} + +VAR(flrceil, 0, 0, 2); + +// finds out z height when cursor points at wall +float +sheight(struct sqr *s, struct sqr *t, float z) +{ + return !flrceil // z-s->floorceil-z + ? (s->type == FHF ? s->floor - t->vdelta / 4.0f : (float)s->floor) + : (s->type == CHF ? s->ceil + t->vdelta / 4.0f : (float)s->ceil); +} + +void +cursorupdate() // called every frame from hud +{ + flrceil = ((int)(player1.pitch >= 0)) * 2; + + volatile float x = + worldpos.x; // volatile needed to prevent msvc7 optimizer bug? + volatile float y = worldpos.y; + volatile float z = worldpos.z; + + cx = (int)x; + cy = (int)y; + + if (OUTBORD(cx, cy)) + return; + struct sqr *s = S(cx, cy); + + // selected wall + if (fabs(sheight(s, s, z) - z) > 1) { + x += x > player1.o.x ? 0.5f : -0.5f; // find right wall cube + y += y > player1.o.y ? 0.5f : -0.5f; + + cx = (int)x; + cy = (int)y; + + if (OUTBORD(cx, cy)) + return; + } + + if (dragging) + makesel(); + + const int GRIDSIZE = 5; + const float GRIDW = 0.5f; + const float GRID8 = 2.0f; + const float GRIDS = 2.0f; + const int GRIDM = 0x7; + + // render editing grid + + for (int ix = cx - GRIDSIZE; ix <= cx + GRIDSIZE; ix++) { + for (int iy = cy - GRIDSIZE; iy <= cy + GRIDSIZE; iy++) { + if (OUTBORD(ix, iy)) + continue; + struct sqr *s = S(ix, iy); + if (SOLID(s)) + continue; + float h1 = sheight(s, s, z); + float h2 = sheight(s, SWS(s, 1, 0, ssize), z); + float h3 = sheight(s, SWS(s, 1, 1, ssize), z); + float h4 = sheight(s, SWS(s, 0, 1, ssize), z); + if (s->tag) + linestyle(GRIDW, 0xFF, 0x40, 0x40); + else if (s->type == FHF || s->type == CHF) + linestyle(GRIDW, 0x80, 0xFF, 0x80); + else + linestyle(GRIDW, 0x80, 0x80, 0x80); + struct block b = { ix, iy, 1, 1 }; + box(&b, h1, h2, h3, h4); + linestyle(GRID8, 0x40, 0x40, 0xFF); + if (!(ix & GRIDM)) + line(ix, iy, h1, ix, iy + 1, h4); + if (!(ix + 1 & GRIDM)) + line(ix + 1, iy, h2, ix + 1, iy + 1, h3); + if (!(iy & GRIDM)) + line(ix, iy, h1, ix + 1, iy, h2); + if (!(iy + 1 & GRIDM)) + line(ix, iy + 1, h4, ix + 1, iy + 1, h3); + } + } + + if (!SOLID(s)) { + float ih = sheight(s, s, z); + linestyle(GRIDS, 0xFF, 0xFF, 0xFF); + struct block b = { cx, cy, 1, 1 }; + box(&b, ih, sheight(s, SWS(s, 1, 0, ssize), z), + sheight(s, SWS(s, 1, 1, ssize), z), + sheight(s, SWS(s, 0, 1, ssize), z)); + linestyle(GRIDS, 0xFF, 0x00, 0x00); + dot(cx, cy, ih); + ch = (int)ih; + } + + if (selset) { + linestyle(GRIDS, 0xFF, 0x40, 0x40); + box(&sel, (float)selh, (float)selh, (float)selh, (float)selh); + } +} + +static OFMutableData *undos; // unlimited undo +VARP(undomegs, 0, 1, 10); // bounded by n megs + +void +pruneundos(int maxremain) // bound memory +{ + int t = 0; + for (ssize_t i = (ssize_t)undos.count - 1; i >= 0; i--) { + struct block *undo = [undos mutableItemAtIndex:i]; + + t += undo->xs * undo->ys * sizeof(struct sqr); + if (t > maxremain) { + OFFreeMemory(undo); + [undos removeItemAtIndex:i]; + } + } +} + +void +makeundo() +{ + if (undos == nil) + undos = + [[OFMutableData alloc] initWithItemSize:sizeof(struct block *)]; + + struct block *copy = blockcopy(&sel); + [undos addItem:©]; + pruneundos(undomegs << 20); +} + +void +editundo() +{ + EDITMP; + if (undos.count == 0) { + conoutf(@"nothing more to undo"); + return; + } + struct block *p = undos.mutableLastItem; + [undos removeLastItem]; + blockpaste(p); + OFFreeMemory(p); +} + +static struct block *copybuf = NULL; + +void +copy() +{ + EDITSELMP; + if (copybuf) + OFFreeMemory(copybuf); + copybuf = blockcopy(&sel); +} + +void +paste() +{ + EDITMP; + if (!copybuf) { + conoutf(@"nothing to paste"); + return; + } + sel.xs = copybuf->xs; + sel.ys = copybuf->ys; + correctsel(); + if (!selset || sel.xs != copybuf->xs || sel.ys != copybuf->ys) { + conoutf(@"incorrect selection"); + return; + } + makeundo(); + copybuf->x = sel.x; + copybuf->y = sel.y; + blockpaste(copybuf); +} + +void +tofronttex() // maintain most recently used of the texture lists when applying + // texture +{ + loopi(3) + { + int c = curedittex[i]; + if (c >= 0) { + uchar *p = hdr.texlists[i]; + int t = p[c]; + for (int a = c - 1; a >= 0; a--) + p[a + 1] = p[a]; + p[0] = t; + curedittex[i] = -1; + } + } +} + +void +editdrag(bool isDown) +{ + if ((dragging = isDown)) { + lastx = cx; + lasty = cy; + lasth = ch; + selset = false; + tofronttex(); + } + makesel(); +} + +// the core editing function. all the *xy functions perform the core operations +// and are also called directly from the network, the function below it is +// strictly triggered locally. They all have very similar structure. + +void +editheightxy(bool isfloor, int amount, const struct block *sel) +{ + loopselxy( + if (isfloor) { + s->floor += amount; + if (s->floor >= s->ceil) + s->floor = s->ceil - 1; + } else { + s->ceil += amount; + if (s->ceil <= s->floor) + s->ceil = s->floor + 1; + }); +} + +void +editheight(int flr, int amount) +{ + EDITSEL; + bool isfloor = flr == 0; + editheightxy(isfloor, amount, &sel); + addmsg(1, 7, SV_EDITH, sel.x, sel.y, sel.xs, sel.ys, isfloor, amount); +} +COMMAND(editheight, ARG_2INT) + +void +edittexxy(int type, int t, const struct block *sel) +{ + loopselxy(switch (type) { + case 0: + s->ftex = t; + break; + case 1: + s->wtex = t; + break; + case 2: + s->ctex = t; + break; + case 3: + s->utex = t; + break; + }); +} + +void +edittex(int type, int dir) +{ + EDITSEL; + if (type < 0 || type > 3) + return; + if (type != lasttype) { + tofronttex(); + lasttype = type; + } + int atype = type == 3 ? 1 : type; + int i = curedittex[atype]; + i = i < 0 ? 0 : i + dir; + curedittex[atype] = i = min(max(i, 0), 255); + int t = lasttex = hdr.texlists[atype][i]; + edittexxy(type, t, &sel); + addmsg(1, 7, SV_EDITT, sel.x, sel.y, sel.xs, sel.ys, type, t); +} + +void +replace() +{ + EDITSELMP; + loop(x, ssize) loop(y, ssize) + { + struct sqr *s = S(x, y); + switch (lasttype) { + case 0: + if (s->ftex == rtex.ftex) + s->ftex = lasttex; + break; + case 1: + if (s->wtex == rtex.wtex) + s->wtex = lasttex; + break; + case 2: + if (s->ctex == rtex.ctex) + s->ctex = lasttex; + break; + case 3: + if (s->utex == rtex.utex) + s->utex = lasttex; + break; + } + } + struct block b = { 0, 0, ssize, ssize }; + remip(&b, 0); +} + +void +edittypexy(int type, const struct block *sel) +{ + loopselxy(s->type = type); +} + +void +edittype(int type) +{ + EDITSEL; + if (type == CORNER && + (sel.xs != sel.ys || sel.xs == 3 || sel.xs > 4 && sel.xs != 8 || + sel.x & ~-sel.xs || sel.y & ~-sel.ys)) { + conoutf(@"corner selection must be power of 2 aligned"); + return; + } + edittypexy(type, &sel); + addmsg(1, 6, SV_EDITS, sel.x, sel.y, sel.xs, sel.ys, type); +} + +void +heightfield(int t) +{ + edittype(t == 0 ? FHF : CHF); +} +COMMAND(heightfield, ARG_1INT) + +void +solid(int t) +{ + edittype(t == 0 ? SPACE : SOLID); +} +COMMAND(solid, ARG_1INT) + +void +corner() +{ + edittype(CORNER); +} +COMMAND(corner, ARG_NONE) + +void +editequalisexy(bool isfloor, const struct block *sel) +{ + int low = 127, hi = -128; + loopselxy({ + if (s->floor < low) + low = s->floor; + if (s->ceil > hi) + hi = s->ceil; + }); + loopselxy({ + if (isfloor) + s->floor = low; + else + s->ceil = hi; + if (s->floor >= s->ceil) + s->floor = s->ceil - 1; + }); +} + +void +equalize(int flr) +{ + bool isfloor = flr == 0; + EDITSEL; + editequalisexy(isfloor, &sel); + addmsg(1, 6, SV_EDITE, sel.x, sel.y, sel.xs, sel.ys, isfloor); +} +COMMAND(equalize, ARG_1INT) + +void +setvdeltaxy(int delta, const struct block *sel) +{ + loopselxy(s->vdelta = max(s->vdelta + delta, 0)); + remipmore(sel, 0); +} + +void +setvdelta(int delta) +{ + EDITSEL; + setvdeltaxy(delta, &sel); + addmsg(1, 6, SV_EDITD, sel.x, sel.y, sel.xs, sel.ys, delta); +} + +const int MAXARCHVERT = 50; +int archverts[MAXARCHVERT][MAXARCHVERT]; +bool archvinit = false; + +void +archvertex(int span, int vert, int delta) +{ + if (!archvinit) { + archvinit = true; + loop(s, MAXARCHVERT) loop(v, MAXARCHVERT) archverts[s][v] = 0; + } + if (span >= MAXARCHVERT || vert >= MAXARCHVERT || span < 0 || vert < 0) + return; + archverts[span][vert] = delta; +} + +void +arch(int sidedelta, int _a) +{ + EDITSELMP; + sel.xs++; + sel.ys++; + if (sel.xs > MAXARCHVERT) + sel.xs = MAXARCHVERT; + if (sel.ys > MAXARCHVERT) + sel.ys = MAXARCHVERT; + struct block *sel_ = &sel; + // Ugly hack to make the macro work. + struct block *sel = sel_; + loopselxy(s->vdelta = sel->xs > sel->ys + ? (archverts[sel->xs - 1][x] + + (y == 0 || y == sel->ys - 1 ? sidedelta : 0)) + : (archverts[sel->ys - 1][y] + + (x == 0 || x == sel->xs - 1 ? sidedelta : 0))); + remipmore(sel, 0); +} + +void +slope(int xd, int yd) +{ + EDITSELMP; + int off = 0; + if (xd < 0) + off -= xd * sel.xs; + if (yd < 0) + off -= yd * sel.ys; + sel.xs++; + sel.ys++; + struct block *sel_ = &sel; + // Ugly hack to make the macro work. + struct block *sel = sel_; + loopselxy(s->vdelta = xd * x + yd * y + off); + remipmore(sel, 0); +} + +void +perlin(int scale, int seed, int psize) +{ + EDITSELMP; + sel.xs++; + sel.ys++; + makeundo(); + sel.xs--; + sel.ys--; + perlinarea(&sel, scale, seed, psize); + sel.xs++; + sel.ys++; + remipmore(&sel, 0); + sel.xs--; + sel.ys--; +} + +VARF( + fullbright, 0, 0, 1, if (fullbright) { + if (noteditmode()) + return; + loopi(mipsize) world[i].r = world[i].g = world[i].b = 176; + }); + +void +edittag(int tag) +{ + EDITSELMP; + struct block *sel_ = &sel; + // Ugly hack to make the macro work. + struct block *sel = sel_; + loopselxy(s->tag = tag); +} + +void +newent(OFString *what, OFString *a1, OFString *a2, OFString *a3, OFString *a4) +{ + EDITSEL; + newentity(sel.x, sel.y, (int)player1.o.z, what, + [a1 cube_intValueWithBase:0], [a2 cube_intValueWithBase:0], + [a3 cube_intValueWithBase:0], [a4 cube_intValueWithBase:0]); +} + +COMMANDN(select, selectpos, ARG_4INT) +COMMAND(edittag, ARG_1INT) +COMMAND(replace, ARG_NONE) +COMMAND(archvertex, ARG_3INT) +COMMAND(arch, ARG_2INT) +COMMAND(slope, ARG_2INT) +COMMANDN(vdelta, setvdelta, ARG_1INT) +COMMANDN(undo, editundo, ARG_NONE) +COMMAND(copy, ARG_NONE) +COMMAND(paste, ARG_NONE) +COMMAND(edittex, ARG_2INT) +COMMAND(newent, ARG_5STR) +COMMAND(perlin, ARG_3INT) DELETED src/editing.mm Index: src/editing.mm ================================================================== --- src/editing.mm +++ /dev/null @@ -1,642 +0,0 @@ -// editing.cpp: most map editing commands go here, entity editing commands are -// in world.cpp - -#include "cube.h" - -#import "DynamicEntity.h" -#import "OFString+Cube.h" - -bool editmode = false; - -// the current selection, used by almost all editing commands -// invariant: all code assumes that these are kept inside MINBORD distance of -// the edge of the map - -block sel; - -OF_CONSTRUCTOR() -{ - enqueueInit(^{ - sel = (block) { - variable(@"selx", 0, 0, 4096, &sel.x, NULL, false), - variable(@"sely", 0, 0, 4096, &sel.y, NULL, false), - variable(@"selxs", 0, 0, 4096, &sel.xs, NULL, false), - variable(@"selys", 0, 0, 4096, &sel.ys, NULL, false), - }; - }); -} - -int selh = 0; -bool selset = false; - -#define loopselxy(b) \ - { \ - makeundo(); \ - loop(x, sel->xs) loop(y, sel->ys) \ - { \ - sqr *s = S(sel->x + x, sel->y + y); \ - b; \ - } \ - remip(sel, 0); \ - } - -int cx, cy, ch; - -int curedittex[] = { -1, -1, -1 }; - -bool dragging = false; -int lastx, lasty, lasth; - -int lasttype = 0, lasttex = 0; -sqr rtex; - -VAR(editing, 0, 0, 1); - -void -toggleedit() -{ - if (player1.state == CS_DEAD) - return; // do not allow dead players to edit to avoid state - // confusion - if (!editmode && !allowedittoggle()) - return; // not in most multiplayer modes - if (!(editmode = !editmode)) { - settagareas(); // reset triggers to allow quick playtesting - entinmap(player1); // find spawn closest to current floating pos - } else { - resettagareas(); // clear trigger areas to allow them to be - // edited - player1.health = 100; - if (m_classicsp) - monsterclear(); // all monsters back at their spawns for - // editing - projreset(); - } - Cube.sharedInstance.repeatsKeys = editmode; - selset = false; - editing = editmode; -} -COMMANDN(edittoggle, toggleedit, ARG_NONE) - -void -correctsel() // ensures above invariant -{ - selset = !OUTBORD(sel.x, sel.y); - int bsize = ssize - MINBORD; - if (sel.xs + sel.x > bsize) - sel.xs = bsize - sel.x; - if (sel.ys + sel.y > bsize) - sel.ys = bsize - sel.y; - if (sel.xs <= 0 || sel.ys <= 0) - selset = false; -} - -bool -noteditmode() -{ - correctsel(); - if (!editmode) - conoutf(@"this function is only allowed in edit mode"); - return !editmode; -} - -bool -noselection() -{ - if (!selset) - conoutf(@"no selection"); - return !selset; -} - -#define EDITSEL \ - if (noteditmode() || noselection()) \ - return; -#define EDITSELMP \ - if (noteditmode() || noselection() || multiplayer()) \ - return; -#define EDITMP \ - if (noteditmode() || multiplayer()) \ - return; - -void -selectpos(int x, int y, int xs, int ys) -{ - block s = { x, y, xs, ys }; - sel = s; - selh = 0; - correctsel(); -} - -void -makesel() -{ - block s = { min(lastx, cx), min(lasty, cy), abs(lastx - cx) + 1, - abs(lasty - cy) + 1 }; - sel = s; - selh = max(lasth, ch); - correctsel(); - if (selset) - rtex = *S(sel.x, sel.y); -} - -VAR(flrceil, 0, 0, 2); - -float -sheight( - sqr *s, sqr *t, float z) // finds out z height when cursor points at wall -{ - return !flrceil // z-s->floorceil-z - ? (s->type == FHF ? s->floor - t->vdelta / 4.0f : (float)s->floor) - : (s->type == CHF ? s->ceil + t->vdelta / 4.0f : (float)s->ceil); -} - -void -cursorupdate() // called every frame from hud -{ - flrceil = ((int)(player1.pitch >= 0)) * 2; - - volatile float x = - worldpos.x; // volatile needed to prevent msvc7 optimizer bug? - volatile float y = worldpos.y; - volatile float z = worldpos.z; - - cx = (int)x; - cy = (int)y; - - if (OUTBORD(cx, cy)) - return; - sqr *s = S(cx, cy); - - // selected wall - if (fabs(sheight(s, s, z) - z) > 1) { - x += x > player1.o.x ? 0.5f : -0.5f; // find right wall cube - y += y > player1.o.y ? 0.5f : -0.5f; - - cx = (int)x; - cy = (int)y; - - if (OUTBORD(cx, cy)) - return; - } - - if (dragging) - makesel(); - - const int GRIDSIZE = 5; - const float GRIDW = 0.5f; - const float GRID8 = 2.0f; - const float GRIDS = 2.0f; - const int GRIDM = 0x7; - - // render editing grid - - for (int ix = cx - GRIDSIZE; ix <= cx + GRIDSIZE; ix++) { - for (int iy = cy - GRIDSIZE; iy <= cy + GRIDSIZE; iy++) { - if (OUTBORD(ix, iy)) - continue; - sqr *s = S(ix, iy); - if (SOLID(s)) - continue; - float h1 = sheight(s, s, z); - float h2 = sheight(s, SWS(s, 1, 0, ssize), z); - float h3 = sheight(s, SWS(s, 1, 1, ssize), z); - float h4 = sheight(s, SWS(s, 0, 1, ssize), z); - if (s->tag) - linestyle(GRIDW, 0xFF, 0x40, 0x40); - else if (s->type == FHF || s->type == CHF) - linestyle(GRIDW, 0x80, 0xFF, 0x80); - else - linestyle(GRIDW, 0x80, 0x80, 0x80); - block b = { ix, iy, 1, 1 }; - box(&b, h1, h2, h3, h4); - linestyle(GRID8, 0x40, 0x40, 0xFF); - if (!(ix & GRIDM)) - line(ix, iy, h1, ix, iy + 1, h4); - if (!(ix + 1 & GRIDM)) - line(ix + 1, iy, h2, ix + 1, iy + 1, h3); - if (!(iy & GRIDM)) - line(ix, iy, h1, ix + 1, iy, h2); - if (!(iy + 1 & GRIDM)) - line(ix, iy + 1, h4, ix + 1, iy + 1, h3); - } - } - - if (!SOLID(s)) { - float ih = sheight(s, s, z); - linestyle(GRIDS, 0xFF, 0xFF, 0xFF); - block b = { cx, cy, 1, 1 }; - box(&b, ih, sheight(s, SWS(s, 1, 0, ssize), z), - sheight(s, SWS(s, 1, 1, ssize), z), - sheight(s, SWS(s, 0, 1, ssize), z)); - linestyle(GRIDS, 0xFF, 0x00, 0x00); - dot(cx, cy, ih); - ch = (int)ih; - } - - if (selset) { - linestyle(GRIDS, 0xFF, 0x40, 0x40); - box(&sel, (float)selh, (float)selh, (float)selh, (float)selh); - } -} - -static OFMutableData *undos; // unlimited undo -VARP(undomegs, 0, 1, 10); // bounded by n megs - -void -pruneundos(int maxremain) // bound memory -{ - int t = 0; - for (ssize_t i = (ssize_t)undos.count - 1; i >= 0; i--) { - block *undo = *(block **)[undos itemAtIndex:i]; - - t += undo->xs * undo->ys * sizeof(sqr); - if (t > maxremain) { - OFFreeMemory(undo); - [undos removeItemAtIndex:i]; - } - } -} - -void -makeundo() -{ - if (undos == nil) - undos = - [[OFMutableData alloc] initWithItemSize:sizeof(block *)]; - - block *copy = blockcopy(&sel); - [undos addItem:©]; - pruneundos(undomegs << 20); -} - -void -editundo() -{ - EDITMP; - if (undos.count == 0) { - conoutf(@"nothing more to undo"); - return; - } - block *p = *(block **)undos.lastItem; - [undos removeLastItem]; - blockpaste(p); - OFFreeMemory(p); -} - -block *copybuf = NULL; - -void -copy() -{ - EDITSELMP; - if (copybuf) - OFFreeMemory(copybuf); - copybuf = blockcopy(&sel); -} - -void -paste() -{ - EDITMP; - if (!copybuf) { - conoutf(@"nothing to paste"); - return; - } - sel.xs = copybuf->xs; - sel.ys = copybuf->ys; - correctsel(); - if (!selset || sel.xs != copybuf->xs || sel.ys != copybuf->ys) { - conoutf(@"incorrect selection"); - return; - } - makeundo(); - copybuf->x = sel.x; - copybuf->y = sel.y; - blockpaste(copybuf); -} - -void -tofronttex() // maintain most recently used of the texture lists when applying - // texture -{ - loopi(3) - { - int c = curedittex[i]; - if (c >= 0) { - uchar *p = hdr.texlists[i]; - int t = p[c]; - for (int a = c - 1; a >= 0; a--) - p[a + 1] = p[a]; - p[0] = t; - curedittex[i] = -1; - } - } -} - -void -editdrag(bool isDown) -{ - if ((dragging = isDown)) { - lastx = cx; - lasty = cy; - lasth = ch; - selset = false; - tofronttex(); - } - makesel(); -} - -// the core editing function. all the *xy functions perform the core operations -// and are also called directly from the network, the function below it is -// strictly triggered locally. They all have very similar structure. - -void -editheightxy(bool isfloor, int amount, const block *sel) -{ - loopselxy( - if (isfloor) { - s->floor += amount; - if (s->floor >= s->ceil) - s->floor = s->ceil - 1; - } else { - s->ceil += amount; - if (s->ceil <= s->floor) - s->ceil = s->floor + 1; - }); -} - -void -editheight(int flr, int amount) -{ - EDITSEL; - bool isfloor = flr == 0; - editheightxy(isfloor, amount, &sel); - addmsg(1, 7, SV_EDITH, sel.x, sel.y, sel.xs, sel.ys, isfloor, amount); -} -COMMAND(editheight, ARG_2INT) - -void -edittexxy(int type, int t, const block *sel) -{ - loopselxy(switch (type) { - case 0: - s->ftex = t; - break; - case 1: - s->wtex = t; - break; - case 2: - s->ctex = t; - break; - case 3: - s->utex = t; - break; - }); -} - -void -edittex(int type, int dir) -{ - EDITSEL; - if (type < 0 || type > 3) - return; - if (type != lasttype) { - tofronttex(); - lasttype = type; - } - int atype = type == 3 ? 1 : type; - int i = curedittex[atype]; - i = i < 0 ? 0 : i + dir; - curedittex[atype] = i = min(max(i, 0), 255); - int t = lasttex = hdr.texlists[atype][i]; - edittexxy(type, t, &sel); - addmsg(1, 7, SV_EDITT, sel.x, sel.y, sel.xs, sel.ys, type, t); -} - -void -replace() -{ - EDITSELMP; - loop(x, ssize) loop(y, ssize) - { - sqr *s = S(x, y); - switch (lasttype) { - case 0: - if (s->ftex == rtex.ftex) - s->ftex = lasttex; - break; - case 1: - if (s->wtex == rtex.wtex) - s->wtex = lasttex; - break; - case 2: - if (s->ctex == rtex.ctex) - s->ctex = lasttex; - break; - case 3: - if (s->utex == rtex.utex) - s->utex = lasttex; - break; - } - } - block b = { 0, 0, ssize, ssize }; - remip(&b, 0); -} - -void -edittypexy(int type, const block *sel) -{ - loopselxy(s->type = type); -} - -void -edittype(int type) -{ - EDITSEL; - if (type == CORNER && - (sel.xs != sel.ys || sel.xs == 3 || sel.xs > 4 && sel.xs != 8 || - sel.x & ~-sel.xs || sel.y & ~-sel.ys)) { - conoutf(@"corner selection must be power of 2 aligned"); - return; - } - edittypexy(type, &sel); - addmsg(1, 6, SV_EDITS, sel.x, sel.y, sel.xs, sel.ys, type); -} - -void -heightfield(int t) -{ - edittype(t == 0 ? FHF : CHF); -} -COMMAND(heightfield, ARG_1INT) - -void -solid(int t) -{ - edittype(t == 0 ? SPACE : SOLID); -} -COMMAND(solid, ARG_1INT) - -void -corner() -{ - edittype(CORNER); -} -COMMAND(corner, ARG_NONE) - -void -editequalisexy(bool isfloor, const block *sel) -{ - int low = 127, hi = -128; - loopselxy({ - if (s->floor < low) - low = s->floor; - if (s->ceil > hi) - hi = s->ceil; - }); - loopselxy({ - if (isfloor) - s->floor = low; - else - s->ceil = hi; - if (s->floor >= s->ceil) - s->floor = s->ceil - 1; - }); -} - -void -equalize(int flr) -{ - bool isfloor = flr == 0; - EDITSEL; - editequalisexy(isfloor, &sel); - addmsg(1, 6, SV_EDITE, sel.x, sel.y, sel.xs, sel.ys, isfloor); -} -COMMAND(equalize, ARG_1INT) - -void -setvdeltaxy(int delta, const block *sel) -{ - loopselxy(s->vdelta = max(s->vdelta + delta, 0)); - remipmore(sel, 0); -} - -void -setvdelta(int delta) -{ - EDITSEL; - setvdeltaxy(delta, &sel); - addmsg(1, 6, SV_EDITD, sel.x, sel.y, sel.xs, sel.ys, delta); -} - -const int MAXARCHVERT = 50; -int archverts[MAXARCHVERT][MAXARCHVERT]; -bool archvinit = false; - -void -archvertex(int span, int vert, int delta) -{ - if (!archvinit) { - archvinit = true; - loop(s, MAXARCHVERT) loop(v, MAXARCHVERT) archverts[s][v] = 0; - } - if (span >= MAXARCHVERT || vert >= MAXARCHVERT || span < 0 || vert < 0) - return; - archverts[span][vert] = delta; -} - -void -arch(int sidedelta, int _a) -{ - EDITSELMP; - sel.xs++; - sel.ys++; - if (sel.xs > MAXARCHVERT) - sel.xs = MAXARCHVERT; - if (sel.ys > MAXARCHVERT) - sel.ys = MAXARCHVERT; - block *sel_ = &sel; - // Ugly hack to make the macro work. - block *sel = sel_; - loopselxy(s->vdelta = sel->xs > sel->ys - ? (archverts[sel->xs - 1][x] + - (y == 0 || y == sel->ys - 1 ? sidedelta : 0)) - : (archverts[sel->ys - 1][y] + - (x == 0 || x == sel->xs - 1 ? sidedelta : 0))); - remipmore(sel, 0); -} - -void -slope(int xd, int yd) -{ - EDITSELMP; - int off = 0; - if (xd < 0) - off -= xd * sel.xs; - if (yd < 0) - off -= yd * sel.ys; - sel.xs++; - sel.ys++; - block *sel_ = &sel; - // Ugly hack to make the macro work. - block *sel = sel_; - loopselxy(s->vdelta = xd * x + yd * y + off); - remipmore(sel, 0); -} - -void -perlin(int scale, int seed, int psize) -{ - EDITSELMP; - sel.xs++; - sel.ys++; - makeundo(); - sel.xs--; - sel.ys--; - perlinarea(&sel, scale, seed, psize); - sel.xs++; - sel.ys++; - remipmore(&sel, 0); - sel.xs--; - sel.ys--; -} - -VARF( - fullbright, 0, 0, 1, if (fullbright) { - if (noteditmode()) - return; - loopi(mipsize) world[i].r = world[i].g = world[i].b = 176; - }); - -void -edittag(int tag) -{ - EDITSELMP; - block *sel_ = &sel; - // Ugly hack to make the macro work. - block *sel = sel_; - loopselxy(s->tag = tag); -} - -void -newent(OFString *what, OFString *a1, OFString *a2, OFString *a3, OFString *a4) -{ - EDITSEL; - newentity(sel.x, sel.y, (int)player1.o.z, what, - [a1 cube_intValueWithBase:0], [a2 cube_intValueWithBase:0], - [a3 cube_intValueWithBase:0], [a4 cube_intValueWithBase:0]); -} - -COMMANDN(select, selectpos, ARG_4INT) -COMMAND(edittag, ARG_1INT) -COMMAND(replace, ARG_NONE) -COMMAND(archvertex, ARG_3INT) -COMMAND(arch, ARG_2INT) -COMMAND(slope, ARG_2INT) -COMMANDN(vdelta, setvdelta, ARG_1INT) -COMMANDN(undo, editundo, ARG_NONE) -COMMAND(copy, ARG_NONE) -COMMAND(paste, ARG_NONE) -COMMAND(edittex, ARG_2INT) -COMMAND(newent, ARG_5STR) -COMMAND(perlin, ARG_3INT) ADDED src/entities.m Index: src/entities.m ================================================================== --- /dev/null +++ src/entities.m @@ -0,0 +1,362 @@ +// entities.cpp: map entity related functions (pickup etc.) + +#include "cube.h" + +#import "DynamicEntity.h" +#import "Entity.h" +#import "MapModelInfo.h" + +OFMutableArray *ents; + +static OFString *entmdlnames[] = { + @"shells", + @"bullets", + @"rockets", + @"rrounds", + @"health", + @"boost", + @"g_armour", + @"y_armour", + @"quad", + @"teleporter", +}; + +int triggertime = 0; + +void +initEntities() +{ + ents = [[OFMutableArray alloc] init]; +} + +static void +renderent(Entity *e, OFString *mdlname, float z, float yaw, int frame/* = 0*/, + int numf/* = 1*/, int basetime/* = 0*/, float speed/* = 10.0f*/) +{ + rendermodel(mdlname, frame, numf, 0, 1.1f, + OFMakeVector3D(e.x, z + S(e.x, e.y)->floor, e.y), yaw, 0, false, + 1.0f, speed, 0, basetime); +} + +void +renderentities() +{ + if (lastmillis > triggertime + 1000) + triggertime = 0; + + for (Entity *e in ents) { + if (e.type == MAPMODEL) { + MapModelInfo *mmi = getmminfo(e.attr2); + if (mmi == nil) + continue; + rendermodel(mmi.name, 0, 1, e.attr4, (float)mmi.rad, + OFMakeVector3D(e.x, + (float)S(e.x, e.y)->floor + mmi.zoff + e.attr3, + e.y), + (float)((e.attr1 + 7) - (e.attr1 + 7) % 15), 0, + false, 1.0f, 10.0f, mmi.snap, 0); + } else { + if (OUTBORD(e.x, e.y)) + continue; + if (e.type != CARROT) { + if (!e.spawned && e.type != TELEPORT) + continue; + if (e.type < I_SHELLS || e.type > TELEPORT) + continue; + renderent(e, entmdlnames[e.type - I_SHELLS], + (float)(1 + + sin(lastmillis / 100.0 + e.x + e.y) / + 20), + lastmillis / 10.0f, 0,1,0,10.0f); + } else { + switch (e.attr2) { + case 1: + case 3: + continue; + + case 2: + case 0: + if (!e.spawned) + continue; + renderent(e, @"carrot", + (float)(1 + + sin(lastmillis / 100.0 + e.x + + e.y) / + 20), + lastmillis / + (e.attr2 ? 1.0f : 10.0f), + 0, 1, 0, 10.0f); + break; + + case 4: + renderent(e, @"switch2", 3, + (float)e.attr3 * 90, + (!e.spawned && !triggertime) ? 1 + : 0, + (e.spawned || !triggertime) ? 1 : 2, + triggertime, 1050.0f); + break; + case 5: + renderent(e, @"switch1", -0.15f, + (float)e.attr3 * 90, + (!e.spawned && !triggertime) ? 30 + : 0, + (e.spawned || !triggertime) ? 1 + : 30, + triggertime, 35.0f); + break; + } + } + } + } +} + +struct itemstat { + int add, max, sound; +} itemstats[] = { + { 10, 50, S_ITEMAMMO }, + { 20, 100, S_ITEMAMMO }, + { 5, 25, S_ITEMAMMO }, + { 5, 25, S_ITEMAMMO }, + { 25, 100, S_ITEMHEALTH }, + { 50, 200, S_ITEMHEALTH }, + { 100, 100, S_ITEMARMOUR }, + { 150, 150, S_ITEMARMOUR }, + { 20000, 30000, S_ITEMPUP }, +}; + +void +baseammo(int gun) +{ + player1.ammo[gun] = itemstats[gun - 1].add * 2; +} + +// these two functions are called when the server acknowledges that you really +// picked up the item (in multiplayer someone may grab it before you). + +static int +radditem(int i, int v) +{ + struct itemstat *is = &itemstats[ents[i].type - I_SHELLS]; + ents[i].spawned = false; + v += is->add; + if (v > is->max) + v = is->max; + playsoundc(is->sound); + return v; +} + +void +realpickup(int n, DynamicEntity *d) +{ + switch (ents[n].type) { + case I_SHELLS: + d.ammo[1] = radditem(n, d.ammo[1]); + break; + case I_BULLETS: + d.ammo[2] = radditem(n, d.ammo[2]); + break; + case I_ROCKETS: + d.ammo[3] = radditem(n, d.ammo[3]); + break; + case I_ROUNDS: + d.ammo[4] = radditem(n, d.ammo[4]); + break; + case I_HEALTH: + d.health = radditem(n, d.health); + break; + case I_BOOST: + d.health = radditem(n, d.health); + break; + + case I_GREENARMOUR: + d.armour = radditem(n, d.armour); + d.armourtype = A_GREEN; + break; + + case I_YELLOWARMOUR: + d.armour = radditem(n, d.armour); + d.armourtype = A_YELLOW; + break; + + case I_QUAD: + d.quadmillis = radditem(n, d.quadmillis); + conoutf(@"you got the quad!"); + break; + } +} + +// these functions are called when the client touches the item + +void +additem(int i, int v, int spawnsec) +{ + // don't pick up if not needed + if (v < itemstats[ents[i].type - I_SHELLS].max) { + // first ask the server for an ack even if someone else gets it + // first + addmsg(1, 3, SV_ITEMPICKUP, i, m_classicsp ? 100000 : spawnsec); + ents[i].spawned = false; + } +} + +// also used by monsters +void +teleport(int n, DynamicEntity *d) +{ + int e = -1, tag = ents[n].attr1, beenhere = -1; + for (;;) { + e = findentity(TELEDEST, e + 1); + if (e == beenhere || e < 0) { + conoutf(@"no teleport destination for tag %d", tag); + return; + } + if (beenhere < 0) + beenhere = e; + if (ents[e].attr2 == tag) { + d.o = OFMakeVector3D(ents[e].x, ents[e].y, ents[e].z); + d.yaw = ents[e].attr1; + d.pitch = 0; + d.vel = OFMakeVector3D(0, 0, 0); + entinmap(d); + playsoundc(S_TELEPORT); + break; + } + } +} + +void +pickup(int n, DynamicEntity *d) +{ + int np = 1; + for (id player in players) + if (player != [OFNull null]) + np++; + // spawn times are dependent on number of players + np = np < 3 ? 4 : (np > 4 ? 2 : 3); + int ammo = np * 2; + switch (ents[n].type) { + case I_SHELLS: + additem(n, d.ammo[1], ammo); + break; + case I_BULLETS: + additem(n, d.ammo[2], ammo); + break; + case I_ROCKETS: + additem(n, d.ammo[3], ammo); + break; + case I_ROUNDS: + additem(n, d.ammo[4], ammo); + break; + case I_HEALTH: + additem(n, d.health, np * 5); + break; + case I_BOOST: + additem(n, d.health, 60); + break; + + case I_GREENARMOUR: + // (100h/100g only absorbs 166 damage) + if (d.armourtype == A_YELLOW && d.armour > 66) + break; + additem(n, d.armour, 20); + break; + + case I_YELLOWARMOUR: + additem(n, d.armour, 20); + break; + + case I_QUAD: + additem(n, d.quadmillis, 60); + break; + + case CARROT: + ents[n].spawned = false; + triggertime = lastmillis; + trigger(ents[n].attr1, ents[n].attr2, + false); // needs to go over server for multiplayer + break; + + case TELEPORT: { + static int lastteleport = 0; + if (lastmillis - lastteleport < 500) + break; + lastteleport = lastmillis; + teleport(n, d); + break; + } + + case JUMPPAD: { + static int lastjumppad = 0; + if (lastmillis - lastjumppad < 300) + break; + lastjumppad = lastmillis; + OFVector3D v = OFMakeVector3D((int)(char)ents[n].attr3 / 10.0f, + (int)(char)ents[n].attr2 / 10.0f, ents[n].attr1 / 10.0f); + player1.vel = OFMakeVector3D(player1.vel.x, player1.vel.y, 0); + vadd(player1.vel, v); + playsoundc(S_JUMPPAD); + break; + } + } +} + +void +checkitems() +{ + if (editmode) + return; + + [ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) { + if (e.type == NOTUSED) + return; + + if (!e.spawned && e.type != TELEPORT && e.type != JUMPPAD) + return; + + if (OUTBORD(e.x, e.y)) + return; + + OFVector3D v = OFMakeVector3D( + e.x, e.y, (float)S(e.x, e.y)->floor + player1.eyeheight); + vdist(dist, t, player1.o, v); + + if (dist < (e.type == TELEPORT ? 4 : 2.5)) + pickup(i, player1); + }]; +} + +void +checkquad(int time) +{ + if (player1.quadmillis && (player1.quadmillis -= time) < 0) { + player1.quadmillis = 0; + playsoundc(S_PUPOUT); + conoutf(@"quad damage is over"); + } +} + +void +putitems(uchar **p) // puts items in network stream and also spawns them locally +{ + [ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) { + if ((e.type >= I_SHELLS && e.type <= I_QUAD) || + e.type == CARROT) { + putint(p, i); + e.spawned = true; + } + }]; +} + +void +resetspawns() +{ + for (Entity *e in ents) + e.spawned = false; +} +void +setspawn(uint i, bool on) +{ + if (i < (uint)ents.count) + ents[i].spawned = on; +} DELETED src/entities.mm Index: src/entities.mm ================================================================== --- src/entities.mm +++ /dev/null @@ -1,361 +0,0 @@ -// entities.cpp: map entity related functions (pickup etc.) - -#include "cube.h" - -#import "DynamicEntity.h" -#import "Entity.h" -#import "MapModelInfo.h" - -OFMutableArray *ents; - -static OFString *entmdlnames[] = { - @"shells", - @"bullets", - @"rockets", - @"rrounds", - @"health", - @"boost", - @"g_armour", - @"y_armour", - @"quad", - @"teleporter", -}; - -int triggertime = 0; - -void -initEntities() -{ - ents = [[OFMutableArray alloc] init]; -} - -void -renderent(Entity *e, OFString *mdlname, float z, float yaw, int frame = 0, - int numf = 1, int basetime = 0, float speed = 10.0f) -{ - rendermodel(mdlname, frame, numf, 0, 1.1f, - OFMakeVector3D(e.x, z + S(e.x, e.y)->floor, e.y), yaw, 0, false, - 1.0f, speed, 0, basetime); -} - -void -renderentities() -{ - if (lastmillis > triggertime + 1000) - triggertime = 0; - - for (Entity *e in ents) { - if (e.type == MAPMODEL) { - MapModelInfo *mmi = getmminfo(e.attr2); - if (mmi == nil) - continue; - rendermodel(mmi.name, 0, 1, e.attr4, (float)mmi.rad, - OFMakeVector3D(e.x, - (float)S(e.x, e.y)->floor + mmi.zoff + e.attr3, - e.y), - (float)((e.attr1 + 7) - (e.attr1 + 7) % 15), 0, - false, 1.0f, 10.0f, mmi.snap, 0); - } else { - if (OUTBORD(e.x, e.y)) - continue; - if (e.type != CARROT) { - if (!e.spawned && e.type != TELEPORT) - continue; - if (e.type < I_SHELLS || e.type > TELEPORT) - continue; - renderent(e, entmdlnames[e.type - I_SHELLS], - (float)(1 + - sin(lastmillis / 100.0 + e.x + e.y) / - 20), - lastmillis / 10.0f); - } else { - switch (e.attr2) { - case 1: - case 3: - continue; - - case 2: - case 0: - if (!e.spawned) - continue; - renderent(e, @"carrot", - (float)(1 + - sin(lastmillis / 100.0 + e.x + - e.y) / - 20), - lastmillis / - (e.attr2 ? 1.0f : 10.0f)); - break; - - case 4: - renderent(e, @"switch2", 3, - (float)e.attr3 * 90, - (!e.spawned && !triggertime) ? 1 - : 0, - (e.spawned || !triggertime) ? 1 : 2, - triggertime, 1050.0f); - break; - case 5: - renderent(e, @"switch1", -0.15f, - (float)e.attr3 * 90, - (!e.spawned && !triggertime) ? 30 - : 0, - (e.spawned || !triggertime) ? 1 - : 30, - triggertime, 35.0f); - break; - } - } - } - } -} - -struct itemstat { - int add, max, sound; -} itemstats[] = { - { 10, 50, S_ITEMAMMO }, - { 20, 100, S_ITEMAMMO }, - { 5, 25, S_ITEMAMMO }, - { 5, 25, S_ITEMAMMO }, - { 25, 100, S_ITEMHEALTH }, - { 50, 200, S_ITEMHEALTH }, - { 100, 100, S_ITEMARMOUR }, - { 150, 150, S_ITEMARMOUR }, - { 20000, 30000, S_ITEMPUP }, -}; - -void -baseammo(int gun) -{ - player1.ammo[gun] = itemstats[gun - 1].add * 2; -} - -// these two functions are called when the server acknowledges that you really -// picked up the item (in multiplayer someone may grab it before you). - -static int -radditem(int i, int v) -{ - itemstat &is = itemstats[ents[i].type - I_SHELLS]; - ents[i].spawned = false; - v += is.add; - if (v > is.max) - v = is.max; - playsoundc(is.sound); - return v; -} - -void -realpickup(int n, DynamicEntity *d) -{ - switch (ents[n].type) { - case I_SHELLS: - d.ammo[1] = radditem(n, d.ammo[1]); - break; - case I_BULLETS: - d.ammo[2] = radditem(n, d.ammo[2]); - break; - case I_ROCKETS: - d.ammo[3] = radditem(n, d.ammo[3]); - break; - case I_ROUNDS: - d.ammo[4] = radditem(n, d.ammo[4]); - break; - case I_HEALTH: - d.health = radditem(n, d.health); - break; - case I_BOOST: - d.health = radditem(n, d.health); - break; - - case I_GREENARMOUR: - d.armour = radditem(n, d.armour); - d.armourtype = A_GREEN; - break; - - case I_YELLOWARMOUR: - d.armour = radditem(n, d.armour); - d.armourtype = A_YELLOW; - break; - - case I_QUAD: - d.quadmillis = radditem(n, d.quadmillis); - conoutf(@"you got the quad!"); - break; - } -} - -// these functions are called when the client touches the item - -void -additem(int i, int v, int spawnsec) -{ - // don't pick up if not needed - if (v < itemstats[ents[i].type - I_SHELLS].max) { - // first ask the server for an ack even if someone else gets it - // first - addmsg(1, 3, SV_ITEMPICKUP, i, m_classicsp ? 100000 : spawnsec); - ents[i].spawned = false; - } -} - -// also used by monsters -void -teleport(int n, DynamicEntity *d) -{ - int e = -1, tag = ents[n].attr1, beenhere = -1; - for (;;) { - e = findentity(TELEDEST, e + 1); - if (e == beenhere || e < 0) { - conoutf(@"no teleport destination for tag %d", tag); - return; - } - if (beenhere < 0) - beenhere = e; - if (ents[e].attr2 == tag) { - d.o = OFMakeVector3D(ents[e].x, ents[e].y, ents[e].z); - d.yaw = ents[e].attr1; - d.pitch = 0; - d.vel = OFMakeVector3D(0, 0, 0); - entinmap(d); - playsoundc(S_TELEPORT); - break; - } - } -} - -void -pickup(int n, DynamicEntity *d) -{ - int np = 1; - for (id player in players) - if (player != [OFNull null]) - np++; - // spawn times are dependent on number of players - np = np < 3 ? 4 : (np > 4 ? 2 : 3); - int ammo = np * 2; - switch (ents[n].type) { - case I_SHELLS: - additem(n, d.ammo[1], ammo); - break; - case I_BULLETS: - additem(n, d.ammo[2], ammo); - break; - case I_ROCKETS: - additem(n, d.ammo[3], ammo); - break; - case I_ROUNDS: - additem(n, d.ammo[4], ammo); - break; - case I_HEALTH: - additem(n, d.health, np * 5); - break; - case I_BOOST: - additem(n, d.health, 60); - break; - - case I_GREENARMOUR: - // (100h/100g only absorbs 166 damage) - if (d.armourtype == A_YELLOW && d.armour > 66) - break; - additem(n, d.armour, 20); - break; - - case I_YELLOWARMOUR: - additem(n, d.armour, 20); - break; - - case I_QUAD: - additem(n, d.quadmillis, 60); - break; - - case CARROT: - ents[n].spawned = false; - triggertime = lastmillis; - trigger(ents[n].attr1, ents[n].attr2, - false); // needs to go over server for multiplayer - break; - - case TELEPORT: { - static int lastteleport = 0; - if (lastmillis - lastteleport < 500) - break; - lastteleport = lastmillis; - teleport(n, d); - break; - } - - case JUMPPAD: { - static int lastjumppad = 0; - if (lastmillis - lastjumppad < 300) - break; - lastjumppad = lastmillis; - OFVector3D v = OFMakeVector3D((int)(char)ents[n].attr3 / 10.0f, - (int)(char)ents[n].attr2 / 10.0f, ents[n].attr1 / 10.0f); - player1.vel = OFMakeVector3D(player1.vel.x, player1.vel.y, 0); - vadd(player1.vel, v); - playsoundc(S_JUMPPAD); - break; - } - } -} - -void -checkitems() -{ - if (editmode) - return; - - [ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) { - if (e.type == NOTUSED) - return; - - if (!e.spawned && e.type != TELEPORT && e.type != JUMPPAD) - return; - - if (OUTBORD(e.x, e.y)) - return; - - OFVector3D v = OFMakeVector3D( - e.x, e.y, (float)S(e.x, e.y)->floor + player1.eyeheight); - vdist(dist, t, player1.o, v); - - if (dist < (e.type == TELEPORT ? 4 : 2.5)) - pickup(i, player1); - }]; -} - -void -checkquad(int time) -{ - if (player1.quadmillis && (player1.quadmillis -= time) < 0) { - player1.quadmillis = 0; - playsoundc(S_PUPOUT); - conoutf(@"quad damage is over"); - } -} - -void -putitems(uchar **p) // puts items in network stream and also spawns them locally -{ - [ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) { - if ((e.type >= I_SHELLS && e.type <= I_QUAD) || - e.type == CARROT) { - putint(p, i); - e.spawned = true; - } - }]; -} - -void -resetspawns() -{ - for (Entity *e in ents) - e.spawned = false; -} -void -setspawn(uint i, bool on) -{ - if (i < (uint)ents.count) - ents[i].spawned = on; -} ADDED src/menus.m Index: src/menus.m ================================================================== --- /dev/null +++ src/menus.m @@ -0,0 +1,168 @@ +// menus.cpp: ingame menu system (also used for scores and serverlist) + +#include "cube.h" + +#import "Menu.h" +#import "MenuItem.h" + +static OFMutableArray *menuStack; +static OFMutableArray *menus; +static int vmenu = -1; + +void +menuset(int menu) +{ + if ((vmenu = menu) >= 1) + resetmovement(player1); + if (vmenu == 1) + menus[1].menusel = 0; +} + +void +showmenu(OFString *name) +{ + int i = 0; + for (Menu *menu in menus) { + if (i > 1 && [menu.name isEqual:name]) { + menuset(i); + return; + } + i++; + } +} +COMMAND(showmenu, ARG_1STR) + +void +sortmenu() +{ + [menus[0].items sort]; +} + +void refreshservers(); + +bool +rendermenu() +{ + if (vmenu < 0) { + [menuStack removeAllObjects]; + return false; + } + + if (vmenu == 1) + refreshservers(); + + Menu *m = menus[vmenu]; + OFString *title; + if (vmenu > 1) + title = [OFString stringWithFormat:@"[ %@ menu ]", m.name]; + else + title = m.name; + int mdisp = m.items.count; + int w = 0; + loopi(mdisp) + { + int x = text_width(m.items[i].text); + if (x > w) + w = x; + } + int tw = text_width(title); + if (tw > w) + w = tw; + int step = FONTH / 4 * 5; + int h = (mdisp + 2) * step; + int y = (VIRTH - h) / 2; + int x = (VIRTW - w) / 2; + blendbox(x - FONTH / 2 * 3, y - FONTH, x + w + FONTH / 2 * 3, + y + h + FONTH, true); + draw_text(title, x, y, 2); + y += FONTH * 2; + if (vmenu) { + int bh = y + m.menusel * step; + blendbox( + x - FONTH, bh - 10, x + w + FONTH, bh + FONTH + 10, false); + } + loopj(mdisp) + { + draw_text(m.items[j].text, x, y, 2); + y += step; + } + return true; +} + +void +newmenu(OFString *name) +{ + if (menus == nil) + menus = [[OFMutableArray alloc] init]; + + [menus addObject:[Menu menuWithName:name]]; +} +COMMAND(newmenu, ARG_1STR) + +void +menumanual(int m, int n, OFString *text) +{ + if (n == 0) + [menus[m].items removeAllObjects]; + + MenuItem *item = [MenuItem itemWithText:text action:@""]; + [menus[m].items addObject:item]; +} + +void +menuitem(OFString *text, OFString *action) +{ + Menu *menu = menus.lastObject; + + MenuItem *item = + [MenuItem itemWithText:text + action:(action.length > 0 ? action : text)]; + [menu.items addObject:item]; +} +COMMAND(menuitem, ARG_2STR) + +bool +menukey(int code, bool isdown) +{ + if (vmenu <= 0) + return false; + + int menusel = menus[vmenu].menusel; + if (isdown) { + if (code == SDLK_ESCAPE) { + menuset(-1); + + if (menuStack.count > 0) { + menuset(menuStack.lastObject.intValue); + [menuStack removeLastObject]; + } + + return true; + } else if (code == SDLK_UP || code == -4) + menusel--; + else if (code == SDLK_DOWN || code == -5) + menusel++; + int n = menus[vmenu].items.count; + if (menusel < 0) + menusel = n - 1; + else if (menusel >= n) + menusel = 0; + menus[vmenu].menusel = menusel; + } else { + if (code == SDLK_RETURN || code == -2) { + OFString *action = menus[vmenu].items[menusel].action; + if (vmenu == 1) + connects(getservername(menusel)); + + if (menuStack == nil) + menuStack = [[OFMutableArray alloc] init]; + + [menuStack addObject:@(vmenu)]; + menuset(-1); + + execute(action, true); + } + } + + return true; +} DELETED src/menus.mm Index: src/menus.mm ================================================================== --- src/menus.mm +++ /dev/null @@ -1,168 +0,0 @@ -// menus.cpp: ingame menu system (also used for scores and serverlist) - -#include "cube.h" - -#import "Menu.h" -#import "MenuItem.h" - -static OFMutableArray *menuStack; -static OFMutableArray *menus; -static int vmenu = -1; - -void -menuset(int menu) -{ - if ((vmenu = menu) >= 1) - resetmovement(player1); - if (vmenu == 1) - menus[1].menusel = 0; -} - -void -showmenu(OFString *name) -{ - int i = 0; - for (Menu *menu in menus) { - if (i > 1 && [menu.name isEqual:name]) { - menuset(i); - return; - } - i++; - } -} -COMMAND(showmenu, ARG_1STR) - -void -sortmenu() -{ - [menus[0].items sort]; -} - -void refreshservers(); - -bool -rendermenu() -{ - if (vmenu < 0) { - [menuStack removeAllObjects]; - return false; - } - - if (vmenu == 1) - refreshservers(); - - Menu *m = menus[vmenu]; - OFString *title; - if (vmenu > 1) - title = [OFString stringWithFormat:@"[ %@ menu ]", m.name]; - else - title = m.name; - int mdisp = m.items.count; - int w = 0; - loopi(mdisp) - { - int x = text_width(m.items[i].text); - if (x > w) - w = x; - } - int tw = text_width(title); - if (tw > w) - w = tw; - int step = FONTH / 4 * 5; - int h = (mdisp + 2) * step; - int y = (VIRTH - h) / 2; - int x = (VIRTW - w) / 2; - blendbox(x - FONTH / 2 * 3, y - FONTH, x + w + FONTH / 2 * 3, - y + h + FONTH, true); - draw_text(title, x, y, 2); - y += FONTH * 2; - if (vmenu) { - int bh = y + m.menusel * step; - blendbox( - x - FONTH, bh - 10, x + w + FONTH, bh + FONTH + 10, false); - } - loopj(mdisp) - { - draw_text(m.items[j].text, x, y, 2); - y += step; - } - return true; -} - -void -newmenu(OFString *name) -{ - if (menus == nil) - menus = [[OFMutableArray alloc] init]; - - [menus addObject:[Menu menuWithName:name]]; -} -COMMAND(newmenu, ARG_1STR) - -void -menumanual(int m, int n, OFString *text) -{ - if (n == 0) - [menus[m].items removeAllObjects]; - - MenuItem *item = [MenuItem itemWithText:text action:@""]; - [menus[m].items addObject:item]; -} - -void -menuitem(OFString *text, OFString *action) -{ - Menu *menu = menus.lastObject; - - MenuItem *item = - [MenuItem itemWithText:text - action:(action.length > 0 ? action : text)]; - [menu.items addObject:item]; -} -COMMAND(menuitem, ARG_2STR) - -bool -menukey(int code, bool isdown) -{ - if (vmenu <= 0) - return false; - - int menusel = menus[vmenu].menusel; - if (isdown) { - if (code == SDLK_ESCAPE) { - menuset(-1); - - if (menuStack.count > 0) { - menuset(menuStack.lastObject.intValue); - [menuStack removeLastObject]; - } - - return true; - } else if (code == SDLK_UP || code == -4) - menusel--; - else if (code == SDLK_DOWN || code == -5) - menusel++; - int n = menus[vmenu].items.count; - if (menusel < 0) - menusel = n - 1; - else if (menusel >= n) - menusel = 0; - menus[vmenu].menusel = menusel; - } else { - if (code == SDLK_RETURN || code == -2) { - OFString *action = menus[vmenu].items[menusel].action; - if (vmenu == 1) - connects(getservername(menusel)); - - if (menuStack == nil) - menuStack = [[OFMutableArray alloc] init]; - - [menuStack addObject:@(vmenu)]; - menuset(-1); - - execute(action, true); - } - } - - return true; -} Index: src/meson.build ================================================================== --- src/meson.build +++ src/meson.build @@ -19,21 +19,21 @@ 'ResolverResult.m', 'ResolverThread.m', 'ServerEntity.m', 'ServerInfo.m', 'Variable.m', - 'clients.mm', - 'clientextras.mm', - 'clientgame.mm', - 'clients2c.mm', + 'clients.m', + 'clientextras.m', + 'clientgame.m', + 'clients2c.m', 'commands.mm', - 'console.mm', - 'editing.mm', - 'entities.mm', + 'console.m', + 'editing.m', + 'entities.m', 'init.mm', - 'menus.mm', - 'monster.mm', + 'menus.m', + 'monster.m', 'physics.mm', 'rendercubes.mm', 'renderextras.mm', 'rendergl.mm', 'rendermd2.mm', ADDED src/monster.m Index: src/monster.m ================================================================== --- /dev/null +++ src/monster.m @@ -0,0 +1,423 @@ +// monster.cpp: implements AI for single player monsters, currently client only + +#include "cube.h" + +#import "DynamicEntity.h" +#import "Entity.h" + +static OFMutableArray *monsters; +static int nextmonster, spawnremain, numkilled, monstertotal, mtimestart; + +VARF(skill, 1, 3, 10, conoutf(@"skill is now %d", skill)); + +OFArray * +getmonsters() +{ + return monsters; +} + +// for savegames +void +restoremonsterstate() +{ + for (DynamicEntity *monster in monsters) + if (monster.state == CS_DEAD) + numkilled++; +} + +#define TOTMFREQ 13 +#define NUMMONSTERTYPES 8 + +struct monstertype // see docs for how these values modify behaviour +{ + short gun, speed, health, freq, lag, rate, pain, loyalty, mscale, + bscale; + short painsound, diesound; + OFConstantString *name, *mdlname; +} + +monstertypes[NUMMONSTERTYPES] = { + { GUN_FIREBALL, 15, 100, 3, 0, 100, 800, 1, 10, 10, S_PAINO, S_DIE1, + @"an ogre", @"monster/ogro" }, + { GUN_CG, 18, 70, 2, 70, 10, 400, 2, 8, 9, S_PAINR, S_DEATHR, + @"a rhino", @"monster/rhino" }, + { GUN_SG, 14, 120, 1, 100, 300, 400, 4, 14, 14, S_PAINE, S_DEATHE, + @"ratamahatta", @"monster/rat" }, + { GUN_RIFLE, 15, 200, 1, 80, 300, 300, 4, 18, 18, S_PAINS, S_DEATHS, + @"a slith", @"monster/slith" }, + { GUN_RL, 13, 500, 1, 0, 100, 200, 6, 24, 24, S_PAINB, S_DEATHB, + @"bauul", @"monster/bauul" }, + { GUN_BITE, 22, 50, 3, 0, 100, 400, 1, 12, 15, S_PAINP, S_PIGGR2, + @"a hellpig", @"monster/hellpig" }, + { GUN_ICEBALL, 12, 250, 1, 0, 10, 400, 6, 18, 18, S_PAINH, S_DEATHH, + @"a knight", @"monster/knight" }, + { GUN_SLIMEBALL, 15, 100, 1, 0, 200, 400, 2, 13, 10, S_PAIND, S_DEATHD, + @"a goblin", @"monster/goblin" }, +}; + +DynamicEntity * +basicmonster(int type, int yaw, int state, int trigger, int move) +{ + if (type >= NUMMONSTERTYPES) { + conoutf(@"warning: unknown monster in spawn: %d", type); + type = 0; + } + DynamicEntity *m = newdynent(); + struct monstertype *t = &monstertypes[(m.mtype = type)]; + m.eyeheight = 2.0f; + m.aboveeye = 1.9f; + m.radius *= t->bscale / 10.0f; + m.eyeheight *= t->bscale / 10.0f; + m.aboveeye *= t->bscale / 10.0f; + m.monsterstate = state; + if (state != M_SLEEP) + spawnplayer(m); + m.trigger = lastmillis + trigger; + m.targetyaw = m.yaw = (float)yaw; + m.move = move; + m.enemy = player1; + m.gunselect = t->gun; + m.maxspeed = (float)t->speed; + m.health = t->health; + m.armour = 0; + loopi(NUMGUNS) m.ammo[i] = 10000; + m.pitch = 0; + m.roll = 0; + m.state = CS_ALIVE; + m.anger = 0; + m.name = t->name; + + if (monsters == nil) + monsters = [[OFMutableArray alloc] init]; + + [monsters addObject:m]; + + return m; +} + +void +spawnmonster() // spawn a random monster according to freq distribution in DMSP +{ + int n = rnd(TOTMFREQ), type; + for (int i = 0;; i++) { + if ((n -= monstertypes[i].freq) < 0) { + type = i; + break; + } + } + basicmonster(type, rnd(360), M_SEARCH, 1000, 1); +} + +// called after map start of when toggling edit mode to reset/spawn all +// monsters to initial state +void +monsterclear() +{ + [monsters removeAllObjects]; + numkilled = 0; + monstertotal = 0; + spawnremain = 0; + if (m_dmsp) { + nextmonster = mtimestart = lastmillis + 10000; + monstertotal = spawnremain = gamemode < 0 ? skill * 10 : 0; + } else if (m_classicsp) { + mtimestart = lastmillis; + + for (Entity *e in ents) { + if (e.type != MONSTER) + continue; + + DynamicEntity *m = + basicmonster(e.attr2, e.attr1, M_SLEEP, 100, 0); + m.o = OFMakeVector3D(e.x, e.y, e.z); + entinmap(m); + monstertotal++; + } + } +} + +// height-correct line of sight for monster shooting/seeing +bool +los(float lx, float ly, float lz, float bx, float by, float bz, OFVector3D *v) +{ + if (OUTBORD((int)lx, (int)ly) || OUTBORD((int)bx, (int)by)) + return false; + float dx = bx - lx; + float dy = by - ly; + int steps = (int)(sqrt(dx * dx + dy * dy) / 0.9); + if (!steps) + return false; + float x = lx; + float y = ly; + int i = 0; + for (;;) { + struct sqr *s = S(fast_f2nat(x), fast_f2nat(y)); + if (SOLID(s)) + break; + float floor = s->floor; + if (s->type == FHF) + floor -= s->vdelta / 4.0f; + float ceil = s->ceil; + if (s->type == CHF) + ceil += s->vdelta / 4.0f; + float rz = lz - ((lz - bz) * (i / (float)steps)); + if (rz < floor || rz > ceil) + break; + v->x = x; + v->y = y; + v->z = rz; + x += dx / (float)steps; + y += dy / (float)steps; + i++; + } + return i >= steps; +} + +bool +enemylos(DynamicEntity *m, OFVector3D *v) +{ + *v = m.o; + return los( + m.o.x, m.o.y, m.o.z, m.enemy.o.x, m.enemy.o.y, m.enemy.o.z, v); +} + +// monster AI is sequenced using transitions: they are in a particular state +// where they execute a particular behaviour until the trigger time is hit, and +// then they reevaluate their situation based on the current state, the +// environment etc., and transition to the next state. Transition timeframes are +// parametrized by difficulty level (skill), faster transitions means quicker +// decision making means tougher AI. + +// n = at skill 0, n/2 = at skill 10, r = added random factor +void +transition(DynamicEntity *m, int state, int moving, int n, int r) +{ + m.monsterstate = state; + m.move = moving; + n = n * 130 / 100; + m.trigger = lastmillis + n - skill * (n / 16) + rnd(r + 1); +} + +void +normalise(DynamicEntity *m, float angle) +{ + while (m.yaw < angle - 180.0f) + m.yaw += 360.0f; + while (m.yaw > angle + 180.0f) + m.yaw -= 360.0f; +} + +// main AI thinking routine, called every frame for every monster +void +monsteraction(DynamicEntity *m) +{ + if (m.enemy.state == CS_DEAD) { + m.enemy = player1; + m.anger = 0; + } + normalise(m, m.targetyaw); + // slowly turn monster towards his target + if (m.targetyaw > m.yaw) { + m.yaw += curtime * 0.5f; + if (m.targetyaw < m.yaw) + m.yaw = m.targetyaw; + } else { + m.yaw -= curtime * 0.5f; + if (m.targetyaw > m.yaw) + m.yaw = m.targetyaw; + } + + vdist(disttoenemy, vectoenemy, m.o, m.enemy.o); + m.pitch = atan2(m.enemy.o.z - m.o.z, disttoenemy) * 180 / PI; + + // special case: if we run into scenery + if (m.blocked) { + m.blocked = false; + // try to jump over obstackle (rare) + if (!rnd(20000 / monstertypes[m.mtype].speed)) + m.jumpnext = true; + // search for a way around (common) + else if (m.trigger < lastmillis && + (m.monsterstate != M_HOME || !rnd(5))) { + // patented "random walk" AI pathfinding (tm) ;) + m.targetyaw += 180 + rnd(180); + transition(m, M_SEARCH, 1, 400, 1000); + } + } + + float enemyyaw = + -(float)atan2(m.enemy.o.x - m.o.x, m.enemy.o.y - m.o.y) / PI * 180 + + 180; + + switch (m.monsterstate) { + case M_PAIN: + case M_ATTACKING: + case M_SEARCH: + if (m.trigger < lastmillis) + transition(m, M_HOME, 1, 100, 200); + break; + + case M_SLEEP: // state classic sp monster start in, wait for visual + // contact + { + OFVector3D target; + if (editmode || !enemylos(m, &target)) + return; // skip running physics + normalise(m, enemyyaw); + float angle = (float)fabs(enemyyaw - m.yaw); + if (disttoenemy < 8 // the better the angle to the player, the + // further the monster can see/hear + || (disttoenemy < 16 && angle < 135) || + (disttoenemy < 32 && angle < 90) || + (disttoenemy < 64 && angle < 45) || angle < 10) { + transition(m, M_HOME, 1, 500, 200); + OFVector3D loc = m.o; + playsound(S_GRUNT1 + rnd(2), &loc); + } + break; + } + + case M_AIMING: + // this state is the delay between wanting to shoot and actually + // firing + if (m.trigger < lastmillis) { + m.lastaction = 0; + m.attacking = true; + OFVector3D attacktarget = m.attacktarget; + shoot(m, &attacktarget); + transition(m, M_ATTACKING, 0, 600, 0); + } + break; + + case M_HOME: + // monster has visual contact, heads straight for player and + // may want to shoot at any time + m.targetyaw = enemyyaw; + if (m.trigger < lastmillis) { + OFVector3D target; + if (!enemylos(m, &target)) { + // no visual contact anymore, let monster get + // as close as possible then search for player + transition(m, M_HOME, 1, 800, 500); + } else { + // the closer the monster is the more likely he + // wants to shoot + if (!rnd((int)disttoenemy / 3 + 1) && + m.enemy.state == CS_ALIVE) { + // get ready to fire + m.attacktarget = target; + transition(m, M_AIMING, 0, + monstertypes[m.mtype].lag, 10); + } else + // track player some more + transition(m, M_HOME, 1, + monstertypes[m.mtype].rate, 0); + } + } + break; + } + + moveplayer(m, 1, false); // use physics to move monster +} + +void +monsterpain(DynamicEntity *m, int damage, DynamicEntity *d) +{ + // a monster hit us + if (d.monsterstate) { + // guard for RL guys shooting themselves :) + if (m != d) { + // don't attack straight away, first get angry + m.anger++; + int anger = m.mtype == d.mtype ? m.anger / 2 : m.anger; + if (anger >= monstertypes[m.mtype].loyalty) + // monster infight if very angry + m.enemy = d; + } + } else { + // player hit us + m.anger = 0; + m.enemy = d; + } + // in this state monster won't attack + transition(m, M_PAIN, 0, monstertypes[m.mtype].pain, 200); + if ((m.health -= damage) <= 0) { + m.state = CS_DEAD; + m.lastaction = lastmillis; + numkilled++; + player1.frags = numkilled; + OFVector3D loc = m.o; + playsound(monstertypes[m.mtype].diesound, &loc); + int remain = monstertotal - numkilled; + if (remain > 0 && remain <= 5) + conoutf(@"only %d monster(s) remaining", remain); + } else { + OFVector3D loc = m.o; + playsound(monstertypes[m.mtype].painsound, &loc); + } +} + +void +endsp(bool allkilled) +{ + conoutf(allkilled ? @"you have cleared the map!" + : @"you reached the exit!"); + conoutf(@"score: %d kills in %d seconds", numkilled, + (lastmillis - mtimestart) / 1000); + monstertotal = 0; + startintermission(); +} + +void +monsterthink() +{ + if (m_dmsp && spawnremain && lastmillis > nextmonster) { + if (spawnremain-- == monstertotal) + conoutf(@"The invasion has begun!"); + nextmonster = lastmillis + 1000; + spawnmonster(); + } + + if (monstertotal && !spawnremain && numkilled == monstertotal) + endsp(true); + + // equivalent of player entity touch, but only teleports are used + [ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) { + if (e.type != TELEPORT) + return; + + if (OUTBORD(e.x, e.y)) + return; + + OFVector3D v = + OFMakeVector3D(e.x, e.y, (float)S(e.x, e.y)->floor); + for (DynamicEntity *monster in monsters) { + if (monster.state == CS_DEAD) { + if (lastmillis - monster.lastaction < 2000) { + monster.move = 0; + moveplayer(monster, 1, false); + } + } else { + v.z += monster.eyeheight; + vdist(dist, t, monster.o, v); + v.z -= monster.eyeheight; + + if (dist < 4) + teleport(i, monster); + } + } + }]; + + for (DynamicEntity *monster in monsters) + if (monster.state == CS_ALIVE) + monsteraction(monster); +} + +void +monsterrender() +{ + for (DynamicEntity *monster in monsters) + renderclient(monster, false, + monstertypes[monster.mtype].mdlname, monster.mtype == 5, + monstertypes[monster.mtype].mscale / 10.0f); +} DELETED src/monster.mm Index: src/monster.mm ================================================================== --- src/monster.mm +++ /dev/null @@ -1,423 +0,0 @@ -// monster.cpp: implements AI for single player monsters, currently client only - -#include "cube.h" - -#import "DynamicEntity.h" -#import "Entity.h" - -static OFMutableArray *monsters; -static int nextmonster, spawnremain, numkilled, monstertotal, mtimestart; - -VARF(skill, 1, 3, 10, conoutf(@"skill is now %d", skill)); - -OFArray * -getmonsters() -{ - return monsters; -} - -// for savegames -void -restoremonsterstate() -{ - for (DynamicEntity *monster in monsters) - if (monster.state == CS_DEAD) - numkilled++; -} - -#define TOTMFREQ 13 -#define NUMMONSTERTYPES 8 - -struct monstertype // see docs for how these values modify behaviour -{ - short gun, speed, health, freq, lag, rate, pain, loyalty, mscale, - bscale; - short painsound, diesound; - OFConstantString *name, *mdlname; -} - -monstertypes[NUMMONSTERTYPES] = { - { GUN_FIREBALL, 15, 100, 3, 0, 100, 800, 1, 10, 10, S_PAINO, S_DIE1, - @"an ogre", @"monster/ogro" }, - { GUN_CG, 18, 70, 2, 70, 10, 400, 2, 8, 9, S_PAINR, S_DEATHR, - @"a rhino", @"monster/rhino" }, - { GUN_SG, 14, 120, 1, 100, 300, 400, 4, 14, 14, S_PAINE, S_DEATHE, - @"ratamahatta", @"monster/rat" }, - { GUN_RIFLE, 15, 200, 1, 80, 300, 300, 4, 18, 18, S_PAINS, S_DEATHS, - @"a slith", @"monster/slith" }, - { GUN_RL, 13, 500, 1, 0, 100, 200, 6, 24, 24, S_PAINB, S_DEATHB, - @"bauul", @"monster/bauul" }, - { GUN_BITE, 22, 50, 3, 0, 100, 400, 1, 12, 15, S_PAINP, S_PIGGR2, - @"a hellpig", @"monster/hellpig" }, - { GUN_ICEBALL, 12, 250, 1, 0, 10, 400, 6, 18, 18, S_PAINH, S_DEATHH, - @"a knight", @"monster/knight" }, - { GUN_SLIMEBALL, 15, 100, 1, 0, 200, 400, 2, 13, 10, S_PAIND, S_DEATHD, - @"a goblin", @"monster/goblin" }, -}; - -DynamicEntity * -basicmonster(int type, int yaw, int state, int trigger, int move) -{ - if (type >= NUMMONSTERTYPES) { - conoutf(@"warning: unknown monster in spawn: %d", type); - type = 0; - } - DynamicEntity *m = newdynent(); - monstertype *t = &monstertypes[(m.mtype = type)]; - m.eyeheight = 2.0f; - m.aboveeye = 1.9f; - m.radius *= t->bscale / 10.0f; - m.eyeheight *= t->bscale / 10.0f; - m.aboveeye *= t->bscale / 10.0f; - m.monsterstate = state; - if (state != M_SLEEP) - spawnplayer(m); - m.trigger = lastmillis + trigger; - m.targetyaw = m.yaw = (float)yaw; - m.move = move; - m.enemy = player1; - m.gunselect = t->gun; - m.maxspeed = (float)t->speed; - m.health = t->health; - m.armour = 0; - loopi(NUMGUNS) m.ammo[i] = 10000; - m.pitch = 0; - m.roll = 0; - m.state = CS_ALIVE; - m.anger = 0; - m.name = t->name; - - if (monsters == nil) - monsters = [[OFMutableArray alloc] init]; - - [monsters addObject:m]; - - return m; -} - -void -spawnmonster() // spawn a random monster according to freq distribution in DMSP -{ - int n = rnd(TOTMFREQ), type; - for (int i = 0;; i++) { - if ((n -= monstertypes[i].freq) < 0) { - type = i; - break; - } - } - basicmonster(type, rnd(360), M_SEARCH, 1000, 1); -} - -// called after map start of when toggling edit mode to reset/spawn all -// monsters to initial state -void -monsterclear() -{ - [monsters removeAllObjects]; - numkilled = 0; - monstertotal = 0; - spawnremain = 0; - if (m_dmsp) { - nextmonster = mtimestart = lastmillis + 10000; - monstertotal = spawnremain = gamemode < 0 ? skill * 10 : 0; - } else if (m_classicsp) { - mtimestart = lastmillis; - - for (Entity *e in ents) { - if (e.type != MONSTER) - continue; - - DynamicEntity *m = - basicmonster(e.attr2, e.attr1, M_SLEEP, 100, 0); - m.o = OFMakeVector3D(e.x, e.y, e.z); - entinmap(m); - monstertotal++; - } - } -} - -// height-correct line of sight for monster shooting/seeing -bool -los(float lx, float ly, float lz, float bx, float by, float bz, OFVector3D &v) -{ - if (OUTBORD((int)lx, (int)ly) || OUTBORD((int)bx, (int)by)) - return false; - float dx = bx - lx; - float dy = by - ly; - int steps = (int)(sqrt(dx * dx + dy * dy) / 0.9); - if (!steps) - return false; - float x = lx; - float y = ly; - int i = 0; - for (;;) { - sqr *s = S(fast_f2nat(x), fast_f2nat(y)); - if (SOLID(s)) - break; - float floor = s->floor; - if (s->type == FHF) - floor -= s->vdelta / 4.0f; - float ceil = s->ceil; - if (s->type == CHF) - ceil += s->vdelta / 4.0f; - float rz = lz - ((lz - bz) * (i / (float)steps)); - if (rz < floor || rz > ceil) - break; - v.x = x; - v.y = y; - v.z = rz; - x += dx / (float)steps; - y += dy / (float)steps; - i++; - } - return i >= steps; -} - -bool -enemylos(DynamicEntity *m, OFVector3D &v) -{ - v = m.o; - return los( - m.o.x, m.o.y, m.o.z, m.enemy.o.x, m.enemy.o.y, m.enemy.o.z, v); -} - -// monster AI is sequenced using transitions: they are in a particular state -// where they execute a particular behaviour until the trigger time is hit, and -// then they reevaluate their situation based on the current state, the -// environment etc., and transition to the next state. Transition timeframes are -// parametrized by difficulty level (skill), faster transitions means quicker -// decision making means tougher AI. - -// n = at skill 0, n/2 = at skill 10, r = added random factor -void -transition(DynamicEntity *m, int state, int moving, int n, int r) -{ - m.monsterstate = state; - m.move = moving; - n = n * 130 / 100; - m.trigger = lastmillis + n - skill * (n / 16) + rnd(r + 1); -} - -void -normalise(DynamicEntity *m, float angle) -{ - while (m.yaw < angle - 180.0f) - m.yaw += 360.0f; - while (m.yaw > angle + 180.0f) - m.yaw -= 360.0f; -} - -// main AI thinking routine, called every frame for every monster -void -monsteraction(DynamicEntity *m) -{ - if (m.enemy.state == CS_DEAD) { - m.enemy = player1; - m.anger = 0; - } - normalise(m, m.targetyaw); - // slowly turn monster towards his target - if (m.targetyaw > m.yaw) { - m.yaw += curtime * 0.5f; - if (m.targetyaw < m.yaw) - m.yaw = m.targetyaw; - } else { - m.yaw -= curtime * 0.5f; - if (m.targetyaw > m.yaw) - m.yaw = m.targetyaw; - } - - vdist(disttoenemy, vectoenemy, m.o, m.enemy.o); - m.pitch = atan2(m.enemy.o.z - m.o.z, disttoenemy) * 180 / PI; - - // special case: if we run into scenery - if (m.blocked) { - m.blocked = false; - // try to jump over obstackle (rare) - if (!rnd(20000 / monstertypes[m.mtype].speed)) - m.jumpnext = true; - // search for a way around (common) - else if (m.trigger < lastmillis && - (m.monsterstate != M_HOME || !rnd(5))) { - // patented "random walk" AI pathfinding (tm) ;) - m.targetyaw += 180 + rnd(180); - transition(m, M_SEARCH, 1, 400, 1000); - } - } - - float enemyyaw = - -(float)atan2(m.enemy.o.x - m.o.x, m.enemy.o.y - m.o.y) / PI * 180 + - 180; - - switch (m.monsterstate) { - case M_PAIN: - case M_ATTACKING: - case M_SEARCH: - if (m.trigger < lastmillis) - transition(m, M_HOME, 1, 100, 200); - break; - - case M_SLEEP: // state classic sp monster start in, wait for visual - // contact - { - OFVector3D target; - if (editmode || !enemylos(m, target)) - return; // skip running physics - normalise(m, enemyyaw); - float angle = (float)fabs(enemyyaw - m.yaw); - if (disttoenemy < 8 // the better the angle to the player, the - // further the monster can see/hear - || (disttoenemy < 16 && angle < 135) || - (disttoenemy < 32 && angle < 90) || - (disttoenemy < 64 && angle < 45) || angle < 10) { - transition(m, M_HOME, 1, 500, 200); - OFVector3D loc = m.o; - playsound(S_GRUNT1 + rnd(2), &loc); - } - break; - } - - case M_AIMING: - // this state is the delay between wanting to shoot and actually - // firing - if (m.trigger < lastmillis) { - m.lastaction = 0; - m.attacking = true; - OFVector3D attacktarget = m.attacktarget; - shoot(m, &attacktarget); - transition(m, M_ATTACKING, 0, 600, 0); - } - break; - - case M_HOME: - // monster has visual contact, heads straight for player and - // may want to shoot at any time - m.targetyaw = enemyyaw; - if (m.trigger < lastmillis) { - OFVector3D target; - if (!enemylos(m, target)) { - // no visual contact anymore, let monster get - // as close as possible then search for player - transition(m, M_HOME, 1, 800, 500); - } else { - // the closer the monster is the more likely he - // wants to shoot - if (!rnd((int)disttoenemy / 3 + 1) && - m.enemy.state == CS_ALIVE) { - // get ready to fire - m.attacktarget = target; - transition(m, M_AIMING, 0, - monstertypes[m.mtype].lag, 10); - } else - // track player some more - transition(m, M_HOME, 1, - monstertypes[m.mtype].rate, 0); - } - } - break; - } - - moveplayer(m, 1, false); // use physics to move monster -} - -void -monsterpain(DynamicEntity *m, int damage, DynamicEntity *d) -{ - // a monster hit us - if (d.monsterstate) { - // guard for RL guys shooting themselves :) - if (m != d) { - // don't attack straight away, first get angry - m.anger++; - int anger = m.mtype == d.mtype ? m.anger / 2 : m.anger; - if (anger >= monstertypes[m.mtype].loyalty) - // monster infight if very angry - m.enemy = d; - } - } else { - // player hit us - m.anger = 0; - m.enemy = d; - } - // in this state monster won't attack - transition(m, M_PAIN, 0, monstertypes[m.mtype].pain, 200); - if ((m.health -= damage) <= 0) { - m.state = CS_DEAD; - m.lastaction = lastmillis; - numkilled++; - player1.frags = numkilled; - OFVector3D loc = m.o; - playsound(monstertypes[m.mtype].diesound, &loc); - int remain = monstertotal - numkilled; - if (remain > 0 && remain <= 5) - conoutf(@"only %d monster(s) remaining", remain); - } else { - OFVector3D loc = m.o; - playsound(monstertypes[m.mtype].painsound, &loc); - } -} - -void -endsp(bool allkilled) -{ - conoutf(allkilled ? @"you have cleared the map!" - : @"you reached the exit!"); - conoutf(@"score: %d kills in %d seconds", numkilled, - (lastmillis - mtimestart) / 1000); - monstertotal = 0; - startintermission(); -} - -void -monsterthink() -{ - if (m_dmsp && spawnremain && lastmillis > nextmonster) { - if (spawnremain-- == monstertotal) - conoutf(@"The invasion has begun!"); - nextmonster = lastmillis + 1000; - spawnmonster(); - } - - if (monstertotal && !spawnremain && numkilled == monstertotal) - endsp(true); - - // equivalent of player entity touch, but only teleports are used - [ents enumerateObjectsUsingBlock:^(Entity *e, size_t i, bool *stop) { - if (e.type != TELEPORT) - return; - - if (OUTBORD(e.x, e.y)) - return; - - OFVector3D v = - OFMakeVector3D(e.x, e.y, (float)S(e.x, e.y)->floor); - for (DynamicEntity *monster in monsters) { - if (monster.state == CS_DEAD) { - if (lastmillis - monster.lastaction < 2000) { - monster.move = 0; - moveplayer(monster, 1, false); - } - } else { - v.z += monster.eyeheight; - vdist(dist, t, monster.o, v); - v.z -= monster.eyeheight; - - if (dist < 4) - teleport(i, monster); - } - } - }]; - - for (DynamicEntity *monster in monsters) - if (monster.state == CS_ALIVE) - monsteraction(monster); -} - -void -monsterrender() -{ - for (DynamicEntity *monster in monsters) - renderclient(monster, false, - monstertypes[monster.mtype].mdlname, monster.mtype == 5, - monstertypes[monster.mtype].mscale / 10.0f); -} Index: src/serverbrowser.mm ================================================================== --- src/serverbrowser.mm +++ src/serverbrowser.mm @@ -200,11 +200,11 @@ } } } } -void +extern "C" void refreshservers() { checkresolver(); checkpings(); if (lastmillis - lastinfo >= 5000)