New buff framework (player implementation)

Get rid of `iConditionBitFlag` in favor of a system of individual buff
objects that get composited to a bitflag on-the-fly.
Buff objects can have callbacks for application, expiration, and tick,
making them pretty flexible. Scripting languages can eventually use
these for custom behavior, too.

TODO:
- Get rid of bitflag in BaseNPC
- Apply buffs from passive nano powers
- Apply buffs from active nano powers
- Move eggs to new system
- ???
This commit is contained in:
gsemaj 2022-07-16 23:33:57 -07:00
parent 306a75f469
commit 98634d5aa2
16 changed files with 309 additions and 54 deletions

View File

@ -50,6 +50,7 @@ CXXSRC=\
src/db/email.cpp\
src/sandbox/seccomp.cpp\
src/sandbox/openbsd.cpp\
src/Buffs.cpp\
src/Chat.cpp\
src/CustomCommands.cpp\
src/Entities.cpp\
@ -96,6 +97,7 @@ CXXHDR=\
vendor/JSON.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Buffs.hpp\
src/Chat.hpp\
src/CustomCommands.hpp\
src/Entities.hpp\

View File

@ -35,13 +35,13 @@ std::vector<EntityRef> Abilities::matchTargets(SkillData* skill, int count, int3
return targets;
}
void Abilities::applyAbility(SkillData* skill, EntityRef src, std::vector<EntityRef> targets) {
void Abilities::useAbility(SkillData* skill, EntityRef src, std::vector<EntityRef> targets) {
for (EntityRef target : targets) {
Entity* entity = target.getEntity();
if (entity->kind != PLAYER && entity->kind != COMBAT_NPC && entity->kind != MOB)
continue; // not a combatant
// TODO abilities
}
}

View File

@ -44,7 +44,7 @@ namespace Abilities {
extern std::map<int32_t, SkillData> SkillTable;
std::vector<EntityRef> matchTargets(SkillData*, int, int32_t*);
void applyAbility(SkillData*, EntityRef, std::vector<EntityRef>);
void useAbility(SkillData*, EntityRef, std::vector<EntityRef>);
void init();
}

30
src/Buffs.cpp Normal file
View File

@ -0,0 +1,30 @@
#include "Buffs.hpp"
#include "PlayerManager.hpp"
using namespace Buffs;
static void timeBuffUpdate(EntityRef self, BuffStack* buff, int type) {
if(self.kind != EntityKind::PLAYER)
return; // not implemented
Player* plr = (Player*)self.getEntity();
if(plr == nullptr)
return;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = buff->id; // eCharStatusTimeBuffID
pkt.eTBU = type; // eTimeBuffUpdate
pkt.eTBT = (buff->buffClass <= BuffClass::CONSUMABLE); // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = plr->getCompositeCondition();
self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
void Buffs::timeBuffUpdateAdd(EntityRef self, BuffStack* buff) {
timeBuffUpdate(self, buff, ETBU_ADD);
}
void Buffs::timeBuffUpdateDelete(EntityRef self, BuffStack* buff) {
timeBuffUpdate(self, buff, ETBU_DEL);
}

122
src/Buffs.hpp Normal file
View File

@ -0,0 +1,122 @@
#pragma once
#include "core/Core.hpp"
#include "Entities.hpp"
#include <assert.h>
#include <vector>
#include <set>
/* forward declaration(s) */
struct BuffStack;
#define CSB_FROM_ECSB(x) (1 << (x - 1))
enum class BuffClass {
NANO = 0,
CONSUMABLE,
EGG,
OTHER
};
typedef void (*BuffCallback)(EntityRef, BuffStack*);
struct BuffStack {
int id; // ECSB
int durationTicks;
EntityRef source;
BuffClass buffClass;
BuffCallback onApply;
BuffCallback onTick;
BuffCallback onExpire;
};
class Buff {
private:
EntityRef self;
std::vector<BuffStack> stacks;
public:
void onTick() {
assert(!stacks.empty());
std::set<BuffCallback> callbacks;
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
if(stack.onTick != nullptr && callbacks.count(stack.onTick) == 0) {
// unique callback
stack.onTick(self, &stack);
callbacks.insert(stack.onTick);
}
if(stack.durationTicks > 0) stack.durationTicks--;
if(stack.durationTicks == 0) {
it = stacks.erase(it);
stack.onExpire(self, &stack);
} else it++;
}
}
void onExpire() {
assert(!stacks.empty());
std::set<BuffCallback> callbacks;
while(!stacks.empty()) {
BuffStack stack = stacks.back();
stacks.pop_back();
if(stack.onExpire != nullptr && callbacks.count(stack.onExpire) == 0) {
// execute unique callback
callbacks.insert(stack.onExpire);
stack.onExpire(self, &stack);
}
}
}
void addStack(BuffStack* stack) {
stacks.push_back(*stack);
}
/*
* Why do this madness? Let me tell you why.
* We need to be able to distinguish whether a player's
* buff is from something like an egg vs. their nano,
* because there are cases where the behavior is different.
* Now, this is impossible to do when they all get composited
* to a single bitfield. So we use a "buff class" and pick
* the buff stack that is most "dominant".
*/
BuffStack* getDominantBuff() {
assert(!stacks.empty());
BuffStack* dominant = nullptr;
for(BuffStack& stack : stacks) {
if(stack.buffClass > dominant->buffClass)
dominant = &stack;
}
return dominant;
}
/*
* In general, a Buff object won't exist
* unless it has stacks. However, when
* popping stacks during iteration (onExpire),
* stacks will be empty for a brief moment
* when the last stack is popped.
*/
bool isStale() {
return stacks.empty();
}
Buff(EntityRef pSelf, BuffStack* firstStack)
: self(pSelf) {
addStack(firstStack);
}
};
namespace Buffs {
void timeBuffUpdateAdd(EntityRef self, BuffStack* buff);
void timeBuffUpdateDelete(EntityRef self, BuffStack* buff);
}

View File

@ -8,14 +8,53 @@
#include "NPCManager.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Buffs.hpp"
#include <assert.h>
#include <iostream>
using namespace Combat;
/// Player Id -> Bullet Id -> Bullet
std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
void Player::addBuff(BuffStack* buff) {
EntityRef self = PlayerManager::getSockFromID(iID);
if(!hasBuff(buff->id)) {
buffs[buff->id] = new Buff(self, buff);
}
else buffs[buff->id]->addStack(buff);
buff->onApply(self, buff);
}
BuffStack* Player::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId]->getDominantBuff();
}
return nullptr;
}
void Player::removeBuff(int buffId) {
if(hasBuff(buffId)) {
buffs[buffId]->onExpire();
delete buffs[buffId];
buffs.erase(buffId);
}
}
bool Player::hasBuff(int buffId) {
return buffs.find(buffId) != buffs.end();
}
int Player::getCompositeCondition() {
int conditionBitFlag = 0;
for(auto buffEntry : buffs) {
if(!buffEntry.second->isStale())
conditionBitFlag |= CSB_FROM_ECSB(buffEntry.first);
}
return conditionBitFlag;
}
int Player::takeDamage(EntityRef src, int amt) {
HP -= amt;
return amt;
@ -41,6 +80,22 @@ void Player::step(time_t currTime) {
// no-op
}
void CombatNPC::addBuff(BuffStack* buff) { /* stubbed */ }
BuffStack* CombatNPC::getBuff(int buffId) { /* stubbed */
return nullptr;
}
void CombatNPC::removeBuff(int buffId) { /* stubbed */ }
bool CombatNPC::hasBuff(int buffId) { /* stubbed */
return false;
}
int CombatNPC::getCompositeCondition() { /* stubbed */
return 0;
}
int CombatNPC::takeDamage(EntityRef src, int amt) {
hp -= amt;
@ -303,17 +358,22 @@ static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag)
plr->iConditionBitFlag ^= CSB_BIT_INFECTION;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1);
pkt1.eCSTB = ECSB_INFECTION; // eCharStatusTimeBuffID
pkt1.eTBU = 1; // eTimeBuffUpdate
pkt1.eTBT = 0; // eTimeBuffType 1 means nano
pkt1.iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
// infection debuff toggles as the client asks it to,
// so we add and remove a permanent debuff
if (pkt->iFlag && !plr->hasBuff(ECSB_INFECTION)) {
BuffStack infection = {
ECSB_INFECTION,
-1, // infinite
sock, // self-inflicted
BuffClass::OTHER,
Buffs::timeBuffUpdateAdd,
nullptr, // client ticks for us! todo anticheat lol
Buffs::timeBuffUpdateDelete
};
plr->addBuff(&infection);
} else if(!pkt->iFlag && plr->hasBuff(ECSB_INFECTION)) {
plr->removeBuff(ECSB_INFECTION);
}
}
static void dealGooDamage(CNSocket *sock, int amount) {
@ -327,12 +387,13 @@ static void dealGooDamage(CNSocket *sock, int amount) {
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) {
BuffStack* protectionBuff = plr->getBuff(ECSB_PROTECT_INFECTION);
if (protectionBuff != nullptr) {
amount = -2; // -2 is the magic number for "Protected" to appear as the damage number
dmg->bProtected = 1;
// eggs allow protection without nanos
if (plr->activeNano != -1 && (plr->iSelfConditionBitFlag & CSB_BIT_PROTECT_INFECTION))
if (protectionBuff->buffClass == BuffClass::NANO && plr->activeNano != -1)
plr->Nanos[plr->activeNano].iStamina -= 3;
} else {
plr->HP -= amount;
@ -356,7 +417,7 @@ static void dealGooDamage(CNSocket *sock, int amount) {
dmg->iID = plr->iID;
dmg->iDamage = amount;
dmg->iHP = plr->HP;
dmg->iConditionBitFlag = plr->iConditionBitFlag;
dmg->iConditionBitFlag = plr->getCompositeCondition();
sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
@ -664,7 +725,7 @@ static void playerTick(CNServer *serv, time_t currTime) {
continue;
// fm patch/lake damage
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION)
if ((plr->hasBuff(ECSB_INFECTION))
&& !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20);
@ -692,7 +753,7 @@ static void playerTick(CNServer *serv, time_t currTime) {
drainRate = skill->batteryUse[boost * 3];
if (skill->drainType == SkillDrainType::PASSIVE) {
// passive buff
// apply passive buff
std::vector<EntityRef> targets;
if (skill->targetType == SkillTargetType::GROUP && plr->group != nullptr)
targets = plr->group->members; // group
@ -700,6 +761,7 @@ static void playerTick(CNServer *serv, time_t currTime) {
targets.push_back(sock); // self
std::cout << "[SKILL] id " << nano.iSkillID << ", type " << skill->skillType << ", target " << (int)skill->targetType << std::endl;
// TODO abilities
}
}
@ -732,6 +794,17 @@ static void playerTick(CNServer *serv, time_t currTime) {
PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
}
// process buffsets
auto it = plr->buffs.begin();
while(it != plr->buffs.end()) {
int buffId = (*it).first;
Buff* buff = (*it).second;
buff->onTick();
if(buff->isStale()) it = plr->buffs.erase(it);
else it++;
}
//
if (transmit) {
INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt);
@ -752,7 +825,7 @@ static void playerTick(CNServer *serv, time_t currTime) {
}
void Combat::init() {
REGISTER_SHARD_TIMER(playerTick, 2000);
REGISTER_SHARD_TIMER(playerTick, MS_PER_PLAYER_TICK);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs);

View File

@ -64,7 +64,7 @@ int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iConditionBitFlag = plr->iConditionBitFlag;
skill->iConditionBitFlag = plr->getCompositeCondition();
}
skillUse->iNPC_ID = eggId;

View File

@ -6,6 +6,10 @@
#include <set>
#include <map>
/* forward declaration(s) */
class Buff;
struct BuffStack;
enum EntityKind {
INVALID,
PLAYER,
@ -88,6 +92,11 @@ public:
ICombatant() {}
virtual ~ICombatant() {}
virtual void addBuff(BuffStack* buff) = 0;
virtual BuffStack* getBuff(int buffId) = 0;
virtual void removeBuff(int buffId) = 0;
virtual bool hasBuff(int buffId) = 0;
virtual int getCompositeCondition() = 0;
virtual int takeDamage(EntityRef, int) = 0;
virtual void heal(EntityRef, int) = 0;
virtual bool isAlive() = 0;
@ -149,6 +158,11 @@ struct CombatNPC : public BaseNPC, public ICombatant {
virtual bool isExtant() override { return hp > 0; }
virtual void addBuff(BuffStack* buff) override;
virtual BuffStack* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual void heal(EntityRef src, int amt) override;
virtual bool isAlive() override;

View File

@ -9,6 +9,7 @@
#include "Eggs.hpp"
#include "MobAI.hpp"
#include "Missions.hpp"
#include "Buffs.hpp"
#include <string.h> // for memset()
#include <assert.h>
@ -484,27 +485,27 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
resp->eST = EST_NANOSTIMPAK;
resp->iSkillID = 144;
int value1 = CSB_BIT_STIMPAKSLOT1 << request->iNanoSlot;
int value2 = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
respdata->eCT = 1;
respdata->iID = player->iID;
respdata->iConditionBitFlag = value1;
respdata->iConditionBitFlag = CSB_FROM_ECSB(eCSB);
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 1; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = player->iConditionBitFlag |= value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
int durationMilliseconds = Abilities::SkillTable[144].durationTime[0] * 100;
BuffStack gumballBuff = {
eCSB,
durationMilliseconds / MS_PER_PLAYER_TICK,
sock,
BuffClass::CONSUMABLE,
Buffs::timeBuffUpdateAdd,
nullptr,
Buffs::timeBuffUpdateDelete,
};
player->addBuff(&gumballBuff);
sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen);
// update inventory serverside
player->Inven[resp->iSlotNum] = resp->RemainItem;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1);
time_t until = getTime() + (time_t)Abilities::SkillTable[144].durationTime[0] * 100;
Eggs::EggBuffs[key] = until;
}
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
@ -757,7 +758,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
plr->money += miscDropType.taroAmount;
// money nano boost
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) {
if (plr->hasBuff(ECSB_REWARD_CASH)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
@ -772,7 +773,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
if (levelDifference > 0)
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
// scavenger nano boost
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) {
if (plr->hasBuff(ECSB_REWARD_BLOB)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;

View File

@ -163,14 +163,14 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
// update player
plr->money += reward->money;
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { // nano boost for taros
if (plr->hasBuff(ECSB_REWARD_CASH)) { // nano boost for taros
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += reward->money * (5 + boost) / 25;
}
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm
if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;

View File

@ -187,7 +187,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
int mobRange = mob->sightRange;
if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH
if (plr->hasBuff(ECSB_UP_STEALTH)
|| Racing::EPRaces.find(s) != Racing::EPRaces.end())
mobRange /= 3;
@ -315,7 +315,7 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
plr->HP -= respdata[i].iDamage;
respdata[i].iHP = plr->HP;
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
respdata[i].iConditionBitFlag = plr->getCompositeCondition();
if (plr->HP <= 0) {
if (!MobAI::aggroCheck(mob, getTime()))

View File

@ -184,7 +184,7 @@ int Nanos::nanoStyle(int nanoID) {
bool Nanos::getNanoBoost(Player* plr) {
for (int i = 0; i < 3; i++)
if (plr->equippedNanos[i] == plr->activeNano)
if (plr->iConditionBitFlag & (CSB_BIT_STIMPAKSLOT1 << i))
if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i))
return true;
return false;
}
@ -209,16 +209,8 @@ static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;
// Unbuff gumballs
int value1 = CSB_BIT_STIMPAKSLOT1 << nano->iNanoSlotNum;
if (plr->iConditionBitFlag & value1) {
int value2 = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 2; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = plr->iConditionBitFlag &= ~value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
}
int buffId = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum;
plr->removeBuff(buffId);
// unsummon nano if replaced
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])

View File

@ -7,6 +7,10 @@
#include <vector>
/* forward declaration(s) */
class Buff;
struct BuffStack;
#define ACTIVE_MISSION_COUNT 6
#define PC_MAXHEALTH(level) (925 + 75 * (level))
@ -32,9 +36,8 @@ struct Player : public Entity, public ICombatant {
int8_t iPCState = 0;
int32_t iWarpLocationFlag = 0;
int64_t aSkywayLocationFlag[2] = {};
int32_t iConditionBitFlag = 0;
int32_t iSelfConditionBitFlag = 0;
int8_t iSpecialState = 0;
std::unordered_map<int, Buff*> buffs = {};
int angle = 0;
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
@ -86,6 +89,11 @@ struct Player : public Entity, public ICombatant {
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
virtual void addBuff(BuffStack* buff) override;
virtual BuffStack* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual void heal(EntityRef src, int amt) override;
virtual bool isAlive() override;

View File

@ -36,6 +36,11 @@ void PlayerManager::removePlayer(CNSocket* key) {
Player* plr = getPlayer(key);
uint64_t fromInstance = plr->instanceID;
// free buff memory
for(auto buffEntry : plr->buffs)
delete buffEntry.second;
// leave group
if(plr->group != nullptr)
Groups::groupKick(plr);
@ -403,7 +408,7 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
switch ((ePCRegenType)reviveData->iRegenType) {
case ePCRegenType::HereByPhoenix: // nano revive
if (!(plr->iConditionBitFlag & CSB_BIT_PHOENIX))
if (!(plr->hasBuff(ECSB_PHOENIX)))
return; // sanity check
plr->Nanos[plr->activeNano].iStamina = 0;
// TODO ABILITIES

View File

@ -118,6 +118,13 @@ enum {
ECSTB__END = 26,
};
enum {
ETBU_NONE = 0,
ETBU_ADD = 1,
ETBU_DEL = 2,
ETBU_CHANGE = 3
};
enum {
SUCC = 1,
FAIL = 0,

View File

@ -7,6 +7,7 @@
#define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr;
#define REGISTER_SHARD_TIMER(handlr, delta) CNShardServer::Timers.push_back(TimerEvent(handlr, delta));
#define MS_PER_PLAYER_TICK 2000
class CNShardServer : public CNServer {
private: