Files
2018-07-17 14:15:37 +02:00

750 lines
20 KiB
C++

#include "projectiles.h"
#include "line3d.h"
#include "main/references.h"
#include "processing.h"
#include "physics/physics_world.h"
#include "obj/object.h"
#include "empire.h"
#include "design/effector.h"
#include "threads.h"
#include "scene/node.h"
#include "scene/particle_system.h"
#include "memory/AllocOnlyPool.h"
#include "threads.h"
#include "obj/lock.h"
#include "scene/animation/anim_projectile.h"
#include "util/save_file.h"
#include "design/design.h"
#include "obj/blueprint.h"
#include "network/network_manager.h"
#include "ISound.h"
#include "ISoundDevice.h"
#include <climits>
#include <algorithm>
memory::AllocOnlyPool<Projectile,threads::Mutex> projPool(8192);
void initNewThread();
void cleanupThread();
struct ProjEffect : public ObjectMessage {
Object* source;
const Effector* effector;
vec3d relImpact;
float effectiveness;
float partiality;
float delay;
ProjEffect(const Effector* fctr, Object* target, Object* src, vec3d relImpactPt, float eff, float part, float del) : ObjectMessage(target), source(src), effector(fctr), relImpact(relImpactPt), effectiveness(eff), partiality(part), delay(del) {
source->grab();
effector->grab();
}
~ProjEffect() {
object->drop();
source->drop();
effector->drop();
}
void process() override {
effector->triggerEffect(source, object, relImpact, effectiveness, partiality, delay);
}
};
struct ProjImpactEffect : public scene::NodeEvent {
const scene::ParticleSystemDesc* system;
vec3d from, vel;
float scale;
float delay;
ProjImpactEffect(scene::Node* parentNode, const scene::ParticleSystemDesc* sys, const vec3d& From, const vec3d& Vel, float Scale, float Delay) : NodeEvent(parentNode), system(sys), from(From), vel(Vel), scale(Scale), delay(Delay) {
}
void process() override {
vec3d pos;
quaterniond rot;
if(node) {
vec3d d = (from - node->abs_position).normalized();
pos = (from - node->abs_position);
rot = quaterniond::fromImpliedTransform(vec3d::front(), d);
}
else {
pos = from;
}
auto* pSys = scene::playParticleSystem(system, node, pos, rot, vel, scale, delay);
if(pSys)
pSys->drop();
}
~ProjImpactEffect() {
}
};
double clamp(double v, double low, double high) {
if(v <= low)
return low;
else if(v >= high)
return high;
else
return v;
}
static inline double sqr(double x) {
return x * x;
}
//Look for a new objec hostile to the owner
Object* findTarget(Empire* owner, const vec3d& position, double radius, const void* ofType = nullptr) {
if(!owner)
return nullptr;
Object* obj = nullptr;
devices.physics->findInBox(AABBoxd::fromCircle(position, radius), [&obj,owner,&position,radius,ofType](const PhysicsItem& item) {
if(obj)
return;
if(item.type == PIT_Object) {
Object* target = item.object;
if(!target->isVisibleTo(owner) || !target->isValid())
return;
if(ofType && target->type != ofType)
return;
if(position.distanceToSQ(target->position) > (radius + target->radius) * (radius + target->radius))
return;
obj = target;
obj->grab();
}
}, owner->hostileMask);
return obj;
}
bool Projectile::tick(double time) {
//Rotate to track the target
if(type == PT_Missile) {
if(target && target->isValid()) {
if(recover <= 0.f) {
vec3d dir = (target->position - position).normalized();
vec3d projImpact = target->position + dir;
if(target->type->blueprintOffset != 0) {
auto* blueprint = (Blueprint*)(((size_t)target) + target->type->blueprintOffset);
const Design* dsg = blueprint->design;
if(dsg != nullptr) {
vec3d impactOffset = target->rotation.inverted() * dir;
projImpact = target->rotation * dsg->hull->getImpact(impactOffset, target->radius, true) + target->position;
}
}
dir = (projImpact - position).normalized();
double speed = velocity.getLength();
vec3d vdir = velocity / speed;
double angle = acos(clamp(vdir.dot(dir),-1,1)), rot = tracking * time;
if(angle < rot) {
velocity = dir * speed;
}
else if(angle >= pi * 0.9) {
quaterniond dirq = quaterniond::fromImpliedTransform(vec3d::front(), dir);
quaterniond vdirq = quaterniond::fromImpliedTransform(vec3d::front(), vdir);
quaterniond result = vdirq.slerp(dirq, rot / angle);
velocity = (result * vec3d::front()).normalized(speed);
}
else {//if(angle < 0.05) {
//Lerp is a good enough approximation
// -- It really is not :( stupid missiles can't even turn 180 degrees
velocity = vdir.interpolate(dir, rot / angle).normalized(speed);
}
if(graphics) {
auto* anim = (scene::ProjectileAnim*)graphics->animator.ptr;
anim->velocity = velocity;
}
}
}
else if(source && source->isValid()) {
if(target) {
auto* prevTarget = target;
target = findTarget(source->owner, target->position, 50.0, target->type);
prevTarget->drop();
}
else {
target = findTarget(source->owner, position, 50.0);
}
//If we didn't quickly find a target, we die twice as fast
if(target == 0 && !effector->type.pierces)
lifetime *= 0.5f;
}
else {
//Become a dummy when the target is already dead
//Also live only half the remaining time
lifetime *= 0.5f;
if(target) {
target->drop();
target = 0;
}
}
}
else if(type == PT_Beam) {
//Beams can't fire when the source is dead
if(!source->isValid())
return true;
if(target) {
if(target->isValid()) {
if(tracking > 0) {
vec3d targDir = (target->position - (source->position + position)).normalize();
double range = velocity.getLength();
vec3d curDir = velocity / range;
double dot = std::max(std::min(targDir.dot(curDir), 1.0), -1.0);
double angDiff = acos(dot);
double track = tracking * time;
if(angDiff <= track)
velocity = targDir * range;
else
velocity = curDir.slerp(targDir,track/angDiff) * range;
}
}
else {
//Our current target is dead, find a new one
auto* prevTarget = target;
if(source)
target = findTarget(source->owner, target->position, 50.0, target->type);
else
target = 0;
prevTarget->drop();
}
}
}
line3dd line(position, vec3d());
if(type != PT_Beam) {
line.end = position + velocity * time;
}
else {
line.start += source->position;
line.end = line.start + velocity;
}
Object* other = 0;
vec3d impactPt;
double prevDistSQ = 0;
double projScale = scale;
//If our line still collides with our last impacted object, we can check for only intervening objects
if(lastImpact && lastImpact->isValid()) {
vec3d closePt = line.getClosestPoint(lastImpact->position,false);
double distSQ = closePt.distanceToSQ(lastImpact->position);
double width = lastImpact->radius + scale;
if(distSQ <= width * width) {
other = lastImpact;
other->grab();
impactPt = closePt - line.getDirection() * sqrt(width*width - distSQ);
if(!effector || !effector->type.pierces)
line.end = impactPt;
prevDistSQ = closePt.distanceToSQ(line.start);
}
}
if(mode != PM_OnlyHitsTarget) {
if(mode == PM_PassthroughInvalid) {
const Effector* eff = effector;
Object* src = source;
devices.physics->findInBox(AABBoxd(line, scale), [&other,&line,&prevDistSQ,&impactPt,projScale,eff,src](const PhysicsItem& item) {
//if(item.type != PIT_Object)
// return;
Object* obj = item.object;
vec3d closePt = line.getClosestPoint(obj->position,false);
double distSQ = closePt.distanceToSQ(obj->position);
double width = obj->radius + projScale;
if(distSQ <= width * width && (!eff || eff->canTarget(src, obj))) {
double objDistSQ = closePt.distanceToSQ(line.start);
if(!other || objDistSQ < prevDistSQ) {
if(other)
other->drop();
other = obj;
other->grab();
prevDistSQ = objDistSQ;
impactPt = closePt - line.getDirection() * sqrt(width*width - distSQ);
}
}
}, source->owner ? (source->owner->hostileMask | 0x1) : ~0x0);
}
else {
devices.physics->findInBox(AABBoxd(line, scale), [&other,&line,&prevDistSQ,&impactPt,projScale](const PhysicsItem& item) {
//if(item.type != PIT_Object)
// return;
Object* obj = item.object;
vec3d closePt = line.getClosestPoint(obj->position,false);
double distSQ = closePt.distanceToSQ(obj->position);
double width = obj->radius + projScale;
if(distSQ <= width * width) {
double objDistSQ = closePt.distanceToSQ(line.start);
if(!other || objDistSQ < prevDistSQ) {
if(other)
other->drop();
other = obj;
other->grab();
prevDistSQ = objDistSQ;
impactPt = closePt - line.getDirection() * sqrt(width*width - distSQ);
}
}
}, source->owner ? (source->owner->hostileMask | 0x1) : ~0x0);
}
}
else if(target && target->isValid()){
vec3d closePt = line.getClosestPoint(target->position,false);
double distSQ = closePt.distanceToSQ(target->position);
double width = target->radius + projScale;
if(distSQ <= width * width) {
if(other)
other->drop();
other = target;
other->grab();
impactPt = closePt - line.getDirection() * sqrt(width*width - distSQ);
}
}
if(other != nullptr && other->type->blueprintOffset != 0) {
auto* blueprint = (Blueprint*)(((size_t)other) + other->type->blueprintOffset);
const Design* dsg = blueprint->design;
if(dsg != nullptr) {
vec3d impactOffset = impactPt - other->position;
impactOffset = other->rotation.inverted() * impactOffset;
impactPt = other->rotation * dsg->hull->getImpact(impactOffset, other->radius, type == PT_Beam) + other->position;
}
}
if(recover > 0.f) {
if(other && other == lastImpact) {
other->drop();
other = nullptr;
}
recover -= time;
if(recover <= 0.f)
recover = 0.f;
}
if(other) {
if(impact)
*impact = impactPt;
if(type == PT_Beam || effector->type.pierces) {
other->grab();
if(lastImpact)
lastImpact->drop();
lastImpact = other;
}
auto* player = Empire::getPlayerEmpire();
if(effector) {
if(player && other->isVisibleTo(player)) {
if(auto* sfx = effector->type.skins[effector->skinIndex].impact_sound) {
if(sfx->loaded && !audio::disableSFX) {
auto* sound = devices.sound->play3D(sfx->source, snd_vec(other->position), false, true);
if(sound) {
if(source)
sound->setVolume((float)((scale + 1.0) / (scale + 7.0)));
sound->setPitch((float)randomd(0.95,1.05));
float dist = other->position.distanceTo(devices.render->cam_pos);
float lo = dist / (dist + scale * 500.0);
if(lo > 0.05)
sound->setLowPass(lo);
sound->resume();
}
}
}
}
}
LockGroup* lockGroup = other->lockGroup;
float delay = 0.f;
if(effector && lockGroup) {
if(type == PT_Missile && missileData) {
double speed = velocity.getLength();
delay = impactPt.distanceTo(position) / speed * 1.5f;
}
if(effector->type.skins[effector->skinIndex].impact && (!player || other->isVisibleTo(player)))
scene::queueNodeEvent(new ProjImpactEffect(other->node, effector->type.skins[effector->skinIndex].impact, impactPt, other->velocity, (float)sqrt(effector->relativeSize * (source ? source->radius : 1.0)), delay));
if(!devices.network->isClient)
lockGroup->addMessage(new ProjEffect(effector, other, source, impactPt - other->position, efficiency, type == PT_Beam ? (float)time : 1.f, delay));
else
other->drop();
}
else {
other->drop();
}
if(type != PT_Beam) {
if(effector && effector->type.pierces) {
recover = effector->type.recoverTime;
if(target && other == target) {
target->drop();
target = nullptr;
}
}
else {
if(type == PT_Missile && missileData) {
auto& data = **missileData;
double gameTime = devices.driver->getGameTime();
double speed = velocity.getLength();
data.lastUpdate = gameTime;
data.aliveUntil = gameTime + delay;
data.pos = impactPt;
data.vel = vec3f((impactPt - position).normalized(speed));
}
return true;
}
}
}
else if(impact) {
*impact = line.end;
}
if(type != PT_Beam) {
position = line.end;
if(type == PT_Missile && missileData) {
auto& data = **missileData;
data.lastUpdate = devices.driver->getGameTime();
data.pos = position;
data.vel = vec3f(velocity);
}
}
lifetime -= (float)time;
return lifetime <= 0;
}
void* Projectile::operator new(size_t size) {
return projPool.alloc();
}
void Projectile::operator delete(void* p) {
return projPool.dealloc((Projectile*)p);
}
Projectile::Projectile(ProjType Type) : lastTick(devices.driver->getGameTime()), source(0), target(0), lastImpact(0), graphics(0), type(Type), impact(0), efficiency(1.f), endNotice(0), mode(PM_Normal), recover(0.f) {
}
Projectile::~Projectile() {
if(graphics) {
graphics->markForDeletion();
graphics->drop();
}
if(type != PT_Missile) {
if(endNotice) {
**endNotice = true;
endNotice->drop();
}
}
else {
if(missileData) {
if((*missileData)->aliveUntil < 0)
(*missileData)->aliveUntil = 0;
missileData->drop();
}
}
if(source)
source->drop();
if(target)
target->drop();
if(lastImpact)
lastImpact->drop();
if(effector)
effector->drop();
}
extern double frameTime_s;
Projectile* Projectile::load(SaveFile& file) {
const Effector* efftr = nullptr;
unsigned char empID;
int dsgId;
unsigned subsysIndex, effectorIndex, effId;
if((bool)file) {
empID = file;
if(empID == INVALID_EMPIRE)
return 0;
dsgId = file;
subsysIndex = file;
effectorIndex = file;
Empire* dsgOwner = Empire::getEmpireByID(empID);
const Design* dsg = dsgOwner->getDesign(dsgId);
if(dsg == nullptr || subsysIndex >= dsg->subsystems.size() || effectorIndex >= dsg->subsystems[subsysIndex].type->effectors.size()) {
//throw "Invalid projectile effector.";
// Just cancel the projectile later so we can continue the load.
}
else {
efftr = &dsg->subsystems[subsysIndex].effectors[effectorIndex]; //No refcounting on design effectors
}
}
else {
effId = file;
efftr = getEffector(effId); //Reference is transfered to projectile
}
//Find effector
Projectile* proj = new Projectile((ProjType)file.read<unsigned char>());
proj->effector = efftr;
//Load data
file >> proj->lastTick;
file >> proj->lifetime;
proj->source = file.readExistingObject();
proj->target = file.readExistingObject();
file >> proj->position;
file >> proj->velocity;
file >> proj->scale;
file >> proj->tracking;
file >> proj->efficiency;
//Actually screw this, these things cause nothing but problems
delete proj;
return 0;
if(!proj->source || !proj->effector) {
//This can happen if the object that fired the projectile
//was already destroyed when the save occured. We have no way
//of instantiating these or their effects at the moment, because
//we know absolutely nothing about the source.
//TODO: Preserve these in some way, they can still impact and matter
//quite a bit.
delete proj;
return 0;
}
//TODO: The effects are non-trivial, figure out how to restore them
//Create graphics
/*proj->graphics = proj->effector->type.createGraphics(proj->effector, proj->scale);
if(!proj->graphics) {
delete proj;
throw "Invalid projectile effector graphics.";
}
proj->graphics->position = proj->position;
proj->graphics->rebuildTransformation();
proj->graphics->animator = new scene::ProjectileAnim(proj->velocity, 1.f);
registerProjectile(proj);
proj->graphics->queueReparent(devices.scene);
return proj;*/
registerProjectile(proj);
return proj;
}
void Projectile::save(SaveFile& file) {
if(effector && effector->effectorId != 0) {
file << false;
file << effector->effectorId;
}
else {
file << true;
if(!effector || !effector->inDesign || !effector->inDesign->owner) {
file << INVALID_EMPIRE;
return;
}
file << effector->inDesign->owner->id;
file << effector->inDesign->id;
file << effector->subsysIndex;
file << effector->effectorIndex;
}
file << (unsigned char)type;
file << lastTick;
file << lifetime;
file << source;
file << target;
file << position;
file << velocity;
file << scale;
file << tracking;
file << efficiency;
}
threads::Mutex projAddLock, projfillLock;
threads::Signal activeProjThreads;
threads::atomic_int pull_index, push_index, processed_count;
std::vector<double> projThreadTimes;
std::vector<Projectile*>* source = new std::vector<Projectile*>, *dest = new std::vector<Projectile*>;
std::vector<Projectile*> queuedProjs;
static void fillProjectiles() {
dest->resize(push_index);
if(!queuedProjs.empty()) {
projAddLock.lock();
dest->insert(dest->end(), queuedProjs.begin(), queuedProjs.end());
queuedProjs.clear();
projAddLock.release();
}
std::swap(source, dest);
dest->resize(source->size());
processed_count = 0;
push_index = 0;
pull_index = 0;
}
volatile bool ProjectilesActive = false;
volatile bool EndProjectiles = false;
volatile bool PauseProjectiles = false;
volatile bool ProjectilesPaused = false;
class ProcessProjectiles : public processing::Action {
bool host, active;
public:
ProcessProjectiles(bool Host) : host(Host), active(true) {
}
~ProcessProjectiles() {
//This happens when the game is ending. We may not complete an entire cycle at the right time.
if(host)
ProjectilesActive = false;
else if(active)
activeProjThreads.signalDown();
}
bool run() {
if(host && activeProjThreads.check(0)) {
if(EndProjectiles)
return true;
ProjectilesActive = true;
if(PauseProjectiles) {
ProjectilesActive = false;
ProjectilesPaused = true;
return false;
}
else if(ProjectilesPaused) {
ProjectilesPaused = false;
}
else {
fillProjectiles();
}
if(!source->empty()) {
activeProjThreads.signal(4);
for(unsigned i = 0; i < 4; ++i)
processing::queueAction(new ProcessProjectiles(false));
}
else {
ProjectilesActive = false;
return false;
}
}
double curTime = devices.driver->getGameTime();
unsigned tickMax = 1000;
while(tickMax--) {
int index = pull_index++;
if(index >= (int)source->size()) {
if(host) {
ProjectilesActive = false;
return false;
}
else {
active = false;
activeProjThreads.signalDown();
return true;
}
}
//Take the next projectile, tick it, and put it on the output stack (unless it needs deleted)
Projectile* proj = source->at(index);
double t = curTime - proj->lastTick;
if(t < 0.125) {
int outIndex = push_index++;
dest->at(outIndex) = proj;
}
else {
if(!proj->tick(t)) {
proj->lastTick = curTime;
int outIndex = push_index++;
dest->at(outIndex) = proj;
}
else {
delete proj;
}
}
}
return false;
}
};
void registerProjectile(Projectile* proj) {
projAddLock.lock();
queuedProjs.push_back(proj);
projAddLock.release();
}
void initProjectiles() {
EndProjectiles = false;
PauseProjectiles = false;
processing::queueAction(new ProcessProjectiles(true));
}
void stopProjectiles() {
EndProjectiles = true;
while(ProjectilesActive)
threads::sleep(1);
activeProjThreads.wait(0);
source->clear(); source->shrink_to_fit();
dest->clear(); dest->shrink_to_fit();
for(auto i = queuedProjs.begin(), end = queuedProjs.end(); i != end; ++i)
delete *i;
queuedProjs.clear();
pull_index = 0;
push_index = 0;
processed_count = 0;
}
void saveProjectiles(SaveFile& file) {
unsigned cnt = (unsigned)(queuedProjs.size() + push_index);
file << cnt;
cnt = (unsigned)queuedProjs.size();
for(unsigned i = 0; i < cnt; ++i)
queuedProjs[i]->save(file);
cnt = (unsigned)push_index;
for(unsigned i = 0; i < cnt; ++i)
(*dest)[i]->save(file);
}
void loadProjectiles(SaveFile& file) {
unsigned cnt = file;
for(unsigned i = 0; i < cnt; ++i)
Projectile::load(file);
}
void pauseProjectiles() {
PauseProjectiles = true;
if(!ProjectilesActive)
return;
activeProjThreads.wait(0);
while(!ProjectilesPaused && ProjectilesActive)
threads::sleep(1);
}
void resumeProjectiles() {
PauseProjectiles = false;
}