645 lines
16 KiB
C++
Raw Normal View History

2013-08-29 11:45:22 +09:00
/*
Copyright (c) 2013 yvt
2013-09-05 00:52:03 +09:00
based on code of pysnip (c) Mathias Kaerlev 2011-2012.
2013-08-29 11:45:22 +09:00
This file is part of OpenSpades.
OpenSpades is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenSpades is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OpenSpades. If not, see <http://www.gnu.org/licenses/>.
*/
2013-08-18 16:18:06 +09:00
#include "Client.h"
2014-03-09 21:43:38 +09:00
#include <Core/Settings.h>
#include <Core/Strings.h>
#include <cstdlib>
#include "Fonts.h"
#include <Core/FileManager.h>
#include <Core/IStream.h>
2013-08-18 16:18:06 +09:00
#include "IAudioChunk.h"
#include "IAudioDevice.h"
2014-03-09 21:43:38 +09:00
2013-08-18 16:18:06 +09:00
#include "CenterMessageView.h"
2014-03-09 21:43:38 +09:00
#include "ChatWindow.h"
2013-08-18 16:18:06 +09:00
#include "MapView.h"
2014-03-09 21:43:38 +09:00
#include "HurtRingView.h"
2013-08-18 16:18:06 +09:00
#include "LimboView.h"
#include "PaletteView.h"
#include "TCProgressView.h"
2013-11-27 16:30:54 +09:00
#include "ClientUI.h"
2014-03-09 21:43:38 +09:00
#include "ScoreboardView.h"
#include "ClientPlayer.h"
#include "ILocalEntity.h"
#include "SmokeSpriteEntity.h"
#include "Corpse.h"
#include "World.h"
#include "Weapon.h"
#include "GameMap.h"
#include "GameMapWrapper.h"
#include "NetClient.h"
2013-08-18 16:18:06 +09:00
static float nextRandom() {
return (float)rand() / (float)RAND_MAX;
}
2013-12-07 17:11:26 +09:00
SPADES_SETTING(cg_chatBeep, "1");
2013-08-18 16:18:06 +09:00
2013-09-07 21:26:06 +09:00
2013-08-18 16:18:06 +09:00
namespace spades {
namespace client {
Client::Client(IRenderer *r, IAudioDevice *audioDev,
const ServerAddress& host, std::string playerName):
2014-03-09 21:43:38 +09:00
renderer(r),
audioDevice(audioDev),
playerName(playerName) ,
2013-12-16 23:27:14 +09:00
hasDelayedReload(false),
2014-03-09 21:43:38 +09:00
hostname(host),
logStream(nullptr),
readyToClose(false),
scoreboardVisible(false),
flashlightOn(false),
inGameLimbo(false),
frameToRendererInit(5),
time(0.f),
timeSinceInit(0.f),
lastAliveTime(0.f),
lastKills(0),
2013-12-16 23:27:14 +09:00
hitFeedbackIconState(0.f),
2014-03-09 21:43:38 +09:00
hitFeedbackFriendly(false),
localFireVibrationTime(-1.f),
lastPosSentTime(0.f),
worldSubFrame(0.f),
grenadeVibration(0.f),
lastMyCorpse(nullptr),
nextScreenShotIndex(0),
nextMapShotIndex(0),
// FIXME: preferences?
corpseSoftTimeLimit(30.f), // FIXME: this is not used
corpseSoftLimit(6),
corpseHardLimit(16)
{
2013-08-18 16:18:06 +09:00
SPADES_MARK_FUNCTION();
2013-08-26 01:27:44 +09:00
SPLog("Initializing...");
2014-03-09 21:43:38 +09:00
designFont.Set(CreateSquareDesignFont(renderer), false);
textFont.Set(CreateGuiFont(renderer), false);
bigTextFont.Set(CreateLargeFont(renderer), false);
2013-08-18 16:18:06 +09:00
renderer->SetFogDistance(128.f);
renderer->SetFogColor(MakeVector3(.8f, 1.f, 1.f));
2014-03-09 21:43:38 +09:00
chatWindow.reset(new ChatWindow(this, GetRenderer(), textFont, false));
killfeedWindow.reset(new ChatWindow(this, GetRenderer(), textFont, true));
hurtRingView.reset(new HurtRingView(this));
centerMessageView.reset(new CenterMessageView(this, bigTextFont));
mapView.reset(new MapView(this, false));
largeMapView.reset(new MapView(this, true));
scoreboard.reset(new ScoreboardView(this));
limbo.reset(new LimboView(this));
paletteView.reset(new PaletteView(this));
tcView.reset(new TCProgressView(this));
2013-11-27 16:30:54 +09:00
scriptedUI.Set(new ClientUI(renderer, audioDev, textFont, this), false);
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
renderer->SetGameMap(nullptr);
2013-08-18 16:18:06 +09:00
}
void Client::SetWorld(spades::client::World *w) {
SPADES_MARK_FUNCTION();
2014-03-09 21:43:38 +09:00
if(world.get() == w){
2013-08-18 16:18:06 +09:00
return;
}
2013-12-02 19:42:38 +09:00
scriptedUI->CloseUI();
2013-08-26 01:27:44 +09:00
2013-08-18 16:18:06 +09:00
RemoveAllCorpses();
2014-03-09 21:43:38 +09:00
RemoveAllLocalEntities();
2013-08-18 16:18:06 +09:00
lastHealth = 0;
lastHurtTime = -100.f;
hurtRingView->ClearAll();
scoreboardVisible = false;
flashlightOn = false;
for(size_t i = 0; i < clientPlayers.size(); i++) {
if(clientPlayers[i]) {
clientPlayers[i]->Invalidate();
}
}
clientPlayers.clear();
2013-08-18 16:18:06 +09:00
if(world){
2014-03-09 21:43:38 +09:00
world->SetListener(nullptr);
renderer->SetGameMap(nullptr);
audioDevice->SetGameMap(nullptr);
world = nullptr;
map = nullptr;
2013-08-18 16:18:06 +09:00
}
2014-03-09 21:43:38 +09:00
world.reset(w);
2013-08-18 16:18:06 +09:00
if(world){
2013-08-26 01:27:44 +09:00
SPLog("World set");
2014-03-09 21:43:38 +09:00
// initialize player view objects
clientPlayers.resize(world->GetNumPlayerSlots());
for(size_t i = 0; i < world->GetNumPlayerSlots(); i++) {
Player *p = world->GetPlayer(i);
if(p){
clientPlayers[i] = new ClientPlayer(p, this);
}else{
2014-03-09 21:43:38 +09:00
clientPlayers[i] = nullptr;
}
}
2013-08-18 16:18:06 +09:00
world->SetListener(this);
map = world->GetMap();
renderer->SetGameMap(map);
audioDevice->SetGameMap(map);
2013-08-26 19:24:15 +09:00
NetLog("------ World Loaded ------");
2013-08-26 01:27:44 +09:00
}else{
SPLog("World removed");
2013-08-26 19:24:15 +09:00
NetLog("------ World Unloaded ------");
2013-08-18 16:18:06 +09:00
}
limbo->SetSelectedTeam(2);
limbo->SetSelectedWeapon(RIFLE_WEAPON);
worldSubFrame = 0.f;
worldSetTime = time;
2014-03-09 21:43:38 +09:00
inGameLimbo = false;
2013-08-18 16:18:06 +09:00
}
Client::~Client() {
SPADES_MARK_FUNCTION();
2013-08-26 19:24:15 +09:00
NetLog("Disconnecting");
if(logStream) {
SPLog("Closing netlog");
2014-03-09 21:43:38 +09:00
logStream.reset();
2013-08-26 19:24:15 +09:00
}
2013-08-26 01:27:44 +09:00
if(net){
SPLog("Disconnecting");
net->Disconnect();
2014-03-09 21:43:38 +09:00
net.reset();
}
2013-08-18 16:18:06 +09:00
2013-08-26 01:27:44 +09:00
SPLog("Disconnected");
2013-08-18 16:18:06 +09:00
RemoveAllLocalEntities();
RemoveAllCorpses();
2014-03-09 21:43:38 +09:00
renderer->SetGameMap(nullptr);
audioDevice->SetGameMap(nullptr);
2013-08-18 16:18:06 +09:00
for(size_t i = 0; i < clientPlayers.size(); i++) {
if(clientPlayers[i]) {
clientPlayers[i]->Invalidate();
}
}
2014-03-09 21:43:38 +09:00
clientPlayers.clear();
2013-08-18 16:18:06 +09:00
2013-11-27 16:30:54 +09:00
scriptedUI->ClientDestroyed();
2014-03-09 21:43:38 +09:00
tcView.reset();
limbo.reset();
scoreboard.reset();
mapView.reset();
largeMapView.reset();
chatWindow.reset();
killfeedWindow.reset();
paletteView.reset();
centerMessageView.reset();
hurtRingView.reset();
designFont.Set(nullptr);
textFont.Set(nullptr);
bigTextFont.Set(nullptr);
world.reset();
}
/** Initiate an initialization which likely to take some time */
void Client::DoInit() {
renderer->Init();
// preload
SmokeSpriteEntity(this, Vector4(), 20.f);
2013-09-06 11:44:36 +09:00
renderer->RegisterImage("Textures/Fluid.png");
renderer->RegisterImage("Textures/WaterExpl.png");
renderer->RegisterImage("Gfx/White.tga");
audioDevice->RegisterSound("Sounds/Weapons/Block/Build.wav");
audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal1.wav");
audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal2.wav");
audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal3.wav");
audioDevice->RegisterSound("Sounds/Weapons/Impacts/FleshLocal4.wav");
audioDevice->RegisterSound("Sounds/Misc/SwitchMapZoom.wav");
audioDevice->RegisterSound("Sounds/Misc/OpenMap.wav");
audioDevice->RegisterSound("Sounds/Misc/CloseMap.wav");
audioDevice->RegisterSound("Sounds/Player/Flashlight.wav");
audioDevice->RegisterSound("Sounds/Weapons/SwitchLocal.wav");
renderer->RegisterImage("Gfx/Ball.png");
renderer->RegisterModel("Models/Player/Dead.kv6");
renderer->RegisterImage("Gfx/Spotlight.tga");
renderer->RegisterImage("Gfx/Glare.tga");
renderer->RegisterModel("Models/Weapons/Spade/Spade.kv6");
renderer->RegisterModel("Models/Weapons/Block/Block2.kv6");
renderer->RegisterModel("Models/Weapons/Grenade/Grenade.kv6");
renderer->RegisterModel("Models/Weapons/SMG/Weapon.kv6");
renderer->RegisterModel("Models/Weapons/SMG/WeaponNoMagazine.kv6");
renderer->RegisterModel("Models/Weapons/SMG/Magazine.kv6");
renderer->RegisterModel("Models/Weapons/Rifle/Weapon.kv6");
renderer->RegisterModel("Models/Weapons/Rifle/WeaponNoMagazine.kv6");
renderer->RegisterModel("Models/Weapons/Rifle/Magazine.kv6");
renderer->RegisterModel("Models/Weapons/Shotgun/Weapon.kv6");
renderer->RegisterModel("Models/Weapons/Shotgun/WeaponNoPump.kv6");
renderer->RegisterModel("Models/Weapons/Shotgun/Pump.kv6");
renderer->RegisterModel("Models/Player/Arm.kv6");
renderer->RegisterModel("Models/Player/UpperArm.kv6");
renderer->RegisterModel("Models/Player/LegCrouch.kv6");
renderer->RegisterModel("Models/Player/TorsoCrouch.kv6");
renderer->RegisterModel("Models/Player/Leg.kv6");
renderer->RegisterModel("Models/Player/Torso.kv6");
renderer->RegisterModel("Models/Player/Arms.kv6");
renderer->RegisterModel("Models/Player/Head.kv6");
renderer->RegisterModel("Models/MapObjects/Intel.kv6");
2013-09-06 11:44:36 +09:00
renderer->RegisterModel("Models/MapObjects/CheckPoint.kv6");
renderer->RegisterImage("Gfx/Sight.tga");
renderer->RegisterImage("Gfx/Bullet/7.62mm.tga");
renderer->RegisterImage("Gfx/Bullet/9mm.tga");
renderer->RegisterImage("Gfx/Bullet/12gauge.tga");
renderer->RegisterImage("Gfx/CircleGradient.png");
2013-09-12 23:26:08 +09:00
renderer->RegisterImage("Gfx/HurtSprite.png");
2013-12-16 22:54:04 +09:00
renderer->RegisterImage("Gfx/HurtRing2.png");
2013-09-06 11:44:36 +09:00
audioDevice->RegisterSound("Sounds/Feedback/Chat.wav");
SPLog("Started connecting to '%s'", hostname.asString(true).c_str());
2014-03-09 21:43:38 +09:00
net.reset(new NetClient(this));
net->Connect(hostname);
// decide log file name
std::string fn = hostname.asString(false);
std::string fn2;
{
time_t t;
struct tm tm;
::time(&t);
tm = *localtime(&t);
char buf[256];
sprintf(buf, "%04d%02d%02d%02d%02d%02d_",
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
tm.tm_sec);
fn2 = buf;
}
for(size_t i = 0; i < fn.size(); i++){
char c = fn[i];
if((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9')) {
fn2 += c;
}else{
fn2 += '_';
}
}
fn2 = "NetLogs/" + fn2 + ".log";
try{
2014-03-09 21:43:38 +09:00
logStream.reset(FileManager::OpenForWriting(fn2.c_str()));
SPLog("Netlog Started at '%s'", fn2.c_str());
}catch(const std::exception& ex){
2013-09-20 23:03:32 +02:00
SPLog("Failed to open netlog file '%s' (%s)", fn2.c_str(), ex.what());
}
}
2013-08-18 16:18:06 +09:00
void Client::RunFrame(float dt) {
SPADES_MARK_FUNCTION();
if(frameToRendererInit > 0){
// waiting for renderer initialization
DrawStartupScreen();
frameToRendererInit--;
if(frameToRendererInit == 0){
DoInit();
}else{
return;
}
}
timeSinceInit += std::min(dt, .03f);
2014-03-09 21:43:38 +09:00
// update network
2013-08-18 16:18:06 +09:00
try{
if(net->GetStatus() == NetClientStatusConnected)
net->DoEvents(0);
else
2013-11-28 00:46:45 +09:00
net->DoEvents(10);
2013-08-18 16:18:06 +09:00
}catch(const std::exception& ex){
2013-08-26 19:24:15 +09:00
if(net->GetStatus() == NetClientStatusNotConnected){
SPLog("Disconnected because of error:\n%s", ex.what());
NetLog("Disconnected because of error:\n%s", ex.what());
2013-08-18 16:18:06 +09:00
throw;
2013-08-26 19:24:15 +09:00
}else{
SPLog("Exception while processing network packets (ignored):\n%s", ex.what());
}
2013-08-18 16:18:06 +09:00
}
hurtRingView->Update(dt);
centerMessageView->Update(dt);
mapView->Update(dt);
largeMapView->Update(dt);
2013-08-18 16:18:06 +09:00
if(world){
2014-03-09 21:43:38 +09:00
UpdateWorld(dt);
2013-08-18 16:18:06 +09:00
}else{
renderer->SetFogColor(MakeVector3(0.f, 0.f, 0.f));
}
chatWindow->Update(dt);
2013-08-26 17:53:14 +09:00
killfeedWindow->Update(dt);
2013-08-18 16:18:06 +09:00
limbo->Update(dt);
2014-03-09 21:43:38 +09:00
// CreateSceneDefinition also can be used for sounds
SceneDefinition sceneDef = CreateSceneDefinition();
2013-08-18 16:18:06 +09:00
lastSceneDef = sceneDef;
// Update sounds
try{
audioDevice->Respatialize(sceneDef.viewOrigin,
sceneDef.viewAxis[2],
sceneDef.viewAxis[1]);
}catch(const std::exception& ex){
2013-08-26 01:27:44 +09:00
SPLog("Audio subsystem returned error (ignored):\n%s",
ex.what());
2013-08-18 16:18:06 +09:00
}
2013-08-18 16:18:06 +09:00
// render scene
DrawScene();
// draw 2d
Draw2D();
2013-11-27 16:30:54 +09:00
// draw scripted GUI
scriptedUI->RunFrame(dt);
if(scriptedUI->WantsClientToBeClosed())
readyToClose = true;
2013-08-18 16:18:06 +09:00
// Well done!
renderer->FrameDone();
renderer->Flip();
// reset all "delayed actions" (in case we forget to reset these)
hasDelayedReload = false;
2013-08-18 16:18:06 +09:00
time += dt;
}
2014-03-09 21:43:38 +09:00
bool Client::IsLimboViewActive(){
if(world){
if(!world->GetLocalPlayer()){
return true;
}else if(inGameLimbo){
return true;
}
2014-02-01 21:55:30 +09:00
}
return false;
}
2014-03-09 21:43:38 +09:00
void Client::SpawnPressed() {
WeaponType weap = limbo->GetSelectedWeapon();
int team = limbo->GetSelectedTeam();
inGameLimbo = false;
if(team == 2)
team = 255;
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
if(!world->GetLocalPlayer()){
// join
net->SendJoin(team, weap,
playerName, lastKills);
2014-03-09 21:43:38 +09:00
}else{
2013-08-18 16:18:06 +09:00
Player *p = world->GetLocalPlayer();
2014-03-09 21:43:38 +09:00
if(p->GetTeamId() != team){
net->SendTeamChange(team);
}
if(team != 2 && p->GetWeapon()->GetWeaponType() != weap){
net->SendWeaponChange(weap);
2013-08-18 16:18:06 +09:00
}
}
}
2014-03-09 21:43:38 +09:00
/** Records chat message/game events to the log file. */
void Client::NetLog(const char *format, ...) {
char buf[4096];
va_list va;
va_start(va, format);
vsprintf(buf, format, va);
va_end(va);
std::string str = buf;
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
time_t t;
struct tm tm;
::time(&t);
tm = *localtime(&t);
2013-11-27 16:30:54 +09:00
2014-03-09 21:43:38 +09:00
std::string timeStr = asctime(&tm);
2013-12-09 17:36:05 +09:00
2014-03-09 21:43:38 +09:00
// remove '\n' in the end of the result of asctime().
timeStr.resize(timeStr.size()-1);
2013-12-02 19:42:38 +09:00
2014-03-09 21:43:38 +09:00
sprintf(buf, "%s %s\n",
timeStr.c_str(), str.c_str());
2013-12-09 17:36:05 +09:00
2014-03-09 21:43:38 +09:00
std::string outStr = EscapeControlCharacters(buf);
printf("%s", outStr.c_str());
2013-12-09 17:36:05 +09:00
2014-03-09 21:43:38 +09:00
if(logStream) {
logStream->Write(outStr);
logStream->Flush();
2013-12-09 17:36:05 +09:00
}
}
2014-03-09 21:43:38 +09:00
#pragma mark - Snapshots
2014-03-09 21:43:38 +09:00
void Client::TakeMapShot(){
2013-11-22 19:34:20 +09:00
try{
std::string name = MapShotPath();
2014-03-09 21:43:38 +09:00
{
std::unique_ptr<IStream> stream(FileManager::OpenForWriting(name.c_str()));
try{
GameMap *map = GetWorld()->GetMap();
if(map == nullptr){
SPRaise("No map loaded");
}
map->Save(stream.get());
}catch(...){
throw;
2013-11-22 19:34:20 +09:00
}
}
std::string msg;
2014-02-15 03:51:29 +09:00
msg = _Tr("Client", "Map saved: {0}", name);
2013-11-22 19:34:20 +09:00
msg = ChatWindow::ColoredMessage(msg, MsgColorSysInfo);
chatWindow->AddMessage(msg);
}catch(const std::exception& ex){
std::string msg;
2014-02-15 03:51:29 +09:00
msg = _Tr("Client", "Saving map failed: ");
2013-11-22 19:34:20 +09:00
std::vector<std::string> lines = SplitIntoLines(ex.what());
msg += lines[0];
msg = ChatWindow::ColoredMessage(msg, MsgColorRed);
chatWindow->AddMessage(msg);
}
}
std::string Client::MapShotPath() {
char buf[256];
for(int i = 0; i < 10000;i++){
sprintf(buf, "Mapshots/shot%04d.vxl", nextScreenShotIndex);
if(FileManager::FileExists(buf)){
nextScreenShotIndex++;
if(nextScreenShotIndex >= 10000)
nextScreenShotIndex = 0;
continue;
}
return buf;
}
SPRaise("No free file name");
}
2014-03-09 21:43:38 +09:00
#pragma mark - Chat Messages
void Client::PlayerSentChatMessage(spades::client::Player *p,
bool global,
const std::string &msg){
{
std::string s;
if(global)
/// prefix added to global chat messages.
s = _Tr("Client", "[Global] ");
s += ChatWindow::TeamColorMessage(p->GetName(), p->GetTeamId());
s += ": ";
s += msg;
chatWindow->AddMessage(s);
2013-09-12 23:30:03 +09:00
}
2014-03-09 21:43:38 +09:00
{
std::string s;
if(global)
s = "[Global] ";
s += p->GetName();
s += ": ";
s += msg;
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
auto col = p->GetTeamId() < 2 ?
world->GetTeam(p->GetTeamId()).color :
IntVector3::Make(255, 255, 255);
scriptedUI->RecordChatLog(s,
MakeVector4(col.x / 255.f, col.y / 255.f,
col.z / 255.f, 0.8f));
2013-08-18 16:18:06 +09:00
}
2014-03-09 21:43:38 +09:00
if(global)
NetLog("[Global] %s (%s): %s",
p->GetName().c_str(),
world->GetTeam(p->GetTeamId()).name.c_str(),
msg.c_str());
else
NetLog("[Team] %s (%s): %s",
p->GetName().c_str(),
world->GetTeam(p->GetTeamId()).name.c_str(),
msg.c_str());
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
if((!IsMuted()) && (int)cg_chatBeep) {
Handle<IAudioChunk> chunk = audioDevice->RegisterSound("Sounds/Feedback/Chat.wav");
audioDevice->PlayLocal(chunk, AudioParam());
2013-08-18 16:18:06 +09:00
}
}
2014-03-09 21:43:38 +09:00
void Client::ServerSentMessage(const std::string &msg) {
chatWindow->AddMessage(msg);
NetLog("%s", msg.c_str());
scriptedUI->RecordChatLog(msg, Vector4::Make(1.f, 1.f, 1.f, 0.8f));
2013-08-18 16:18:06 +09:00
}
2014-03-09 21:43:38 +09:00
#pragma mark - Follow / Spectate
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
void Client::FollowNextPlayer(bool reverse) {
int myTeam = 2;
if(world->GetLocalPlayer()){
myTeam = world->GetLocalPlayer()->GetTeamId();
}
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
int nextId = followingPlayerId;
do{
reverse ? --nextId : ++nextId;
if(nextId >= static_cast<int>(world->GetNumPlayerSlots()))
nextId = 0;
if(nextId < 0)
nextId = world->GetNumPlayerSlots() - 1;
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
Player *p = world->GetPlayer(nextId);
if(p == nullptr)
continue;
if(myTeam < 2 && p->GetTeamId() != myTeam)
continue;
if(p->GetFront().GetPoweredLength() < .01f)
continue;
2013-08-18 16:18:06 +09:00
2014-03-09 21:43:38 +09:00
break;
}while(nextId != followingPlayerId);
followingPlayerId = nextId;
}
bool Client::IsFollowing() {
if(!world) return false;
if(!world->GetLocalPlayer())
return false;
Player *p = world->GetLocalPlayer();
if(p->GetTeamId() >= 2)
return true;
if(p->IsAlive())
return false;
else return true;
}
2013-08-18 16:18:06 +09:00
}
}