diff --git a/README.md b/README.md index 241380849..d847024bb 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,23 @@ OpenSpades is a compatible client of Ace of Spades 0.75. 2. Or [open an issue](https://github.com/yvt/openspades/issues) if the problem persists ### On Linux + #### Snap package On [snap enabled](https://snapcraft.io/docs/core/install) systems, the latest pre-built stable release of OpenSpades can be installed with: ```bash sudo snap install openspades ``` +Once installed, you'll be able to launch OpenSpades from inside the desktop menu or from your terminal with the `openspades` + +#### Flatpak package +On [flatpak enabled](https://flatpak.org/setup/) systems, OpenSpades can be installed with: + +```bash +flatpak install flathub jp.yvt.OpenSpades +``` -Once installed, you'll be able to launch OpenSpades from inside the desktop menu or from your terminal with the `openspades` command. +Once installed, you'll be able to launch OpenSpades from inside the desktop menu or from your terminal with `flatpak run jp.yvt.OpenSpades` #### Building and installing from source GCC 4.9 / Clang 3.2 or later is recommended because OpenSpades relies on C++11 features heavily. diff --git a/Resources/Shaders/DynamicLight/Common.fs b/Resources/Shaders/DynamicLight/Common.fs index 5c69ba973..b4d3e390a 100644 --- a/Resources/Shaders/DynamicLight/Common.fs +++ b/Resources/Shaders/DynamicLight/Common.fs @@ -46,8 +46,10 @@ varying vec3 lightNormal; varying vec3 lightTexCoord; vec3 EvaluateDynamicLightNoBump() { - if(lightTexCoord.z < 0.) discard; - + if (lightTexCoord.z < 0. || any(lessThan(lightTexCoord.xy, vec2(0.0))) || + any(greaterThan(lightTexCoord.xy, vec2(lightTexCoord.z)))) + discard; + // diffuse lighting float intensity = dot(normalize(lightPos), normalize(lightNormal)); if(intensity < 0.) discard; diff --git a/Sources/Client/ClientPlayer.cpp b/Sources/Client/ClientPlayer.cpp index 047524e6d..5971255d3 100644 --- a/Sources/Client/ClientPlayer.cpp +++ b/Sources/Client/ClientPlayer.cpp @@ -26,7 +26,6 @@ #include "ClientPlayer.h" #include "GameMap.h" #include "GunCasing.h" -#include "GunCasing.h" #include "IAudioChunk.h" #include "IAudioDevice.h" #include "IImage.h" @@ -199,6 +198,7 @@ namespace spades { time = 0.f; viewWeaponOffset = MakeVector3(0, 0, 0); lastFront = MakeVector3(0, 0, 0); + flashlightOrientation = p->GetFront(); ScriptContextHandle ctx; IRenderer *renderer = client->GetRenderer(); @@ -302,8 +302,6 @@ namespace spades { PlayerInput actualInput = player->GetInput(); WeaponInput actualWeapInput = player->GetWeaponInput(); - Vector3 vel = player->GetVelocty(); - vel.z = 0.f; if (actualInput.sprint && player->IsAlive()) { sprintState += dt * 4.f; if (sprintState > 1.f) @@ -442,6 +440,19 @@ namespace spades { } } + { + // Smooth the flashlight's movement + Vector3 diff = player->GetFront() - flashlightOrientation; + float sq = diff.GetLength(); + if (sq > 0.1) { + flashlightOrientation += diff.Normalize() * (sq - 0.1); + } + + flashlightOrientation = + Mix(flashlightOrientation, player->GetFront(), 1.0f - powf(1.0e-6, dt)) + .Normalize(); + } + // FIXME: should do for non-active skins? asIScriptObject *skin; if (ShouldRenderInThirdPersonView()) { @@ -594,6 +605,16 @@ namespace spades { } } + std::array ClientPlayer::GetFlashlightAxes() { + std::array axes; + + axes[2] = flashlightOrientation; + axes[0] = Vector3::Cross(flashlightOrientation, player->GetUp()).Normalize(); + axes[1] = Vector3::Cross(axes[0], axes[2]); + + return axes; + } + void ClientPlayer::AddToSceneFirstPersonView() { Player *p = player; IRenderer *renderer = client->GetRenderer(); @@ -617,9 +638,7 @@ namespace spades { light.radius = 40.f; light.type = DynamicLightTypeSpotlight; light.spotAngle = 30.f * M_PI / 180.f; - light.spotAxis[0] = p->GetRight(); - light.spotAxis[1] = p->GetUp(); - light.spotAxis[2] = p->GetFront(); + light.spotAxis = GetFlashlightAxes(); light.image = renderer->RegisterImage("Gfx/Spotlight.png"); renderer->AddLight(light); diff --git a/Sources/Client/ClientPlayer.h b/Sources/Client/ClientPlayer.h index ac5c7bf58..574f2a8a9 100644 --- a/Sources/Client/ClientPlayer.h +++ b/Sources/Client/ClientPlayer.h @@ -20,6 +20,8 @@ #pragma once +#include + #include "Player.h" #include #include @@ -60,6 +62,8 @@ namespace spades { Vector3 lastFront; + Vector3 flashlightOrientation; + asIScriptObject *spadeSkin; asIScriptObject *blockSkin; asIScriptObject *weaponSkin; @@ -72,6 +76,7 @@ namespace spades { Handle sandboxedRenderer; + std::array GetFlashlightAxes(); void AddToSceneThirdPersonView(); void AddToSceneFirstPersonView(); diff --git a/Sources/Client/IRenderer.h b/Sources/Client/IRenderer.h index 5e8b7792b..1034cc003 100644 --- a/Sources/Client/IRenderer.h +++ b/Sources/Client/IRenderer.h @@ -21,7 +21,9 @@ #pragma once #include +#include +#include #include "IImage.h" #include "IModel.h" #include "SceneDefinition.h" @@ -56,7 +58,7 @@ namespace spades { float radius; Vector3 color; - Vector3 spotAxis[3]; + std::array spotAxis; IImage *image; float spotAngle; diff --git a/Sources/Client/NetClient.cpp b/Sources/Client/NetClient.cpp index 1e21733d6..5869fdb48 100644 --- a/Sources/Client/NetClient.cpp +++ b/Sources/Client/NetClient.cpp @@ -424,7 +424,6 @@ namespace spades { status = NetClientStatusConnecting; statusString = _Tr("NetClient", "Connecting to the server"); - timeToTryMapLoad = 0; } void NetClient::Disconnect() { @@ -529,8 +528,6 @@ namespace spades { mapSize = reader.ReadInt(); status = NetClientStatusReceivingMap; statusString = _Tr("NetClient", "Loading snapshot"); - timeToTryMapLoad = 30; - tryMapLoadOnPacketType = true; } } else if (status == NetClientStatusReceivingMap) { if (event.type == ENET_EVENT_TYPE_RECEIVE) { @@ -541,60 +538,29 @@ namespace spades { dt.erase(dt.begin()); mapData.insert(mapData.end(), dt.begin(), dt.end()); - timeToTryMapLoad = 200; - statusString = _Tr("NetClient", "Loading snapshot ({0}/{1})", mapData.size(), mapSize); - if (mapSize == mapData.size()) { - status = NetClientStatusConnected; - statusString = _Tr("NetClient", "Connected"); - - try { - MapLoaded(); - } catch (const std::exception &ex) { - if (strstr(ex.what(), "File truncated") || - strstr(ex.what(), "EOF reached")) { - SPLog("Map decoder returned error. Maybe we will get more " - "data...:\n%s", - ex.what()); - // hack: more data to load... - status = NetClientStatusReceivingMap; - statusString = _Tr("NetClient", "Still loading..."); - } else { - Disconnect(); - statusString = _Tr("NetClient", "Error"); - throw; - } - - } catch (...) { - Disconnect(); - statusString = _Tr("NetClient", "Error"); - throw; - } - } - } else { reader.DumpDebug(); - // On pyspades and derivative servers the actual size of the map data - // cannot be known in beforehand, so we have to find the end of the data - // by one of other means. One indicator for this would be a packet of a - // type other than MapChunk, which usually marks the end of map data - // transfer. + // The actual size of the map data cannot be known beforehand because + // of compression. This means we must detect the end of the map + // transfer in another way. // - // However, we can't rely on this heuristics entirely because there are - // several occasions where the server would send non-MapChunk packets - // during map loading sequence, for example: + // We do this by checking for a StateData packet, which is sent + // directly after the map transfer completes. + // + // A number of other packets can also be received while loading the map: // // - World update packets (WorldUpdate, ExistingPlayer, and // CreatePlayer) for the current round. We must store such packets - // temporarily and process them later when a `World` is created. + // temporarily and process them later when a `World` is created. // // - Leftover reload packet from the previous round. This happens when // you initiate the reload action and a map change occurs before it - // is completed. In pyspades, sending a reload packet is implemented - // by registering a callback function to the Twisted reactor. This + // is completed. In pyspades, sending a reload packet is implemented + // by registering a callback function to the Twisted reactor. This // callback function sends a reload packet, but it does not check if // the current game round is finished, nor is it unregistered on a // map change. @@ -604,29 +570,18 @@ namespace spades { // an "invalid player ID" exception, so we simply drop it during // map load sequence. // - if (reader.GetType() == PacketTypeWeaponReload) { - // Drop reload packets - } else if (reader.GetType() != PacketTypeWorldUpdate && - reader.GetType() != PacketTypeExistingPlayer && - reader.GetType() != PacketTypeCreatePlayer && - tryMapLoadOnPacketType) { + + if (reader.GetType() == PacketTypeStateData) { status = NetClientStatusConnected; statusString = _Tr("NetClient", "Connected"); try { MapLoaded(); } catch (const std::exception &ex) { - tryMapLoadOnPacketType = false; if (strstr(ex.what(), "File truncated") || strstr(ex.what(), "EOF reached")) { - SPLog("Map decoder returned error. Maybe we will get more " - "data...:\n%s", + SPLog("Map decoder returned error:\n%s", ex.what()); - // hack: more data to load... - status = NetClientStatusReceivingMap; - statusString = _Tr("NetClient", "Still loading..."); - goto stillLoading; - } else { Disconnect(); statusString = _Tr("NetClient", "Error"); throw; @@ -637,12 +592,15 @@ namespace spades { throw; } HandleGamePacket(reader); - } else { - stillLoading: + } else if (reader.GetType() == PacketTypeWeaponReload) { + // Drop the reload packet. Pyspades does not + // cancel the reload packets on map change and + // they would cause an error if we would + // process them + } else { + // Save the packet for later savedPackets.push_back(reader.GetData()); } - - // HandleGamePacket(reader); } } } else if (status == NetClientStatusConnected) { @@ -660,37 +618,6 @@ namespace spades { } } } - - if (status == NetClientStatusReceivingMap) { - if (timeToTryMapLoad > 0) { - timeToTryMapLoad--; - if (timeToTryMapLoad == 0) { - try { - MapLoaded(); - } catch (const std::exception &ex) { - if ((strstr(ex.what(), "File truncated") || - strstr(ex.what(), "EOF reached")) && - savedPackets.size() < 400) { - // hack: more data to load... - SPLog( - "Map decoder returned error. Maybe we will get more data...:\n%s", - ex.what()); - status = NetClientStatusReceivingMap; - statusString = _Tr("NetClient", "Still loading..."); - timeToTryMapLoad = 200; - } else { - Disconnect(); - statusString = _Tr("NetClient", "Error"); - throw; - } - } catch (...) { - Disconnect(); - statusString = _Tr("NetClient", "Error"); - throw; - } - } - } - } } World *NetClient::GetWorld() { return client->GetWorld(); } diff --git a/Sources/Draw/GLDynamicLight.cpp b/Sources/Draw/GLDynamicLight.cpp index 869310bc3..74ad82e0c 100644 --- a/Sources/Draw/GLDynamicLight.cpp +++ b/Sources/Draw/GLDynamicLight.cpp @@ -25,11 +25,11 @@ namespace spades { GLDynamicLight::GLDynamicLight(const client::DynamicLightParam ¶m) : param(param) { if (param.type == client::DynamicLightTypeSpotlight) { - float t = tanf(param.spotAngle * .5f) * 2.f; + float t = tanf(param.spotAngle * .5f); Matrix4 mat; mat = Matrix4::FromAxis(param.spotAxis[0], param.spotAxis[1], param.spotAxis[2], param.origin); - mat = mat * Matrix4::Scale(t, t, 1.f); + mat = mat * Matrix4::Scale(t * 2.0f, t * 2.0f, 1.f); projMatrix = mat.InversedFast(); @@ -40,7 +40,55 @@ namespace spades { m.m[8] += .5f; m.m[9] += .5f; projMatrix = m * projMatrix; + + // Construct clipping planes which are oriented inside. + // To do that, first we calculate tangent vectors: + Vector3 planeTan[] = { + param.spotAxis[2] + param.spotAxis[0] * t, + param.spotAxis[2] + param.spotAxis[1] * t, + param.spotAxis[2] - param.spotAxis[0] * t, + param.spotAxis[2] - param.spotAxis[1] * t, + }; + // Then, use them to derive normal vectors: + Vector3 planeN[] = { + Vector3::Cross(param.spotAxis[1], planeTan[0]), + Vector3::Cross(planeTan[1], param.spotAxis[0]), + Vector3::Cross(planeTan[2], param.spotAxis[1]), + Vector3::Cross(param.spotAxis[0], planeTan[3]), + }; + // Finally, find planes with these normal vectors: + for (std::size_t i = 0; i < 4; ++i) { + clipPlanes[i] = Plane3::PlaneWithPointOnPlane(param.origin, planeN[i]); + } + } + } + + bool GLDynamicLight::Cull(const spades::AABB3 &box) const { + if (param.type == client::DynamicLightTypeSpotlight) { + for (const Plane3 &plane : clipPlanes) { + if (!PlaneCullTest(plane, box)) { + return false; + } + } + } + + const client::DynamicLightParam ¶m = GetParam(); + return box.Inflate(param.radius) && param.origin; + } + + bool GLDynamicLight::SphereCull(const spades::Vector3 ¢er, float radius) const { + const client::DynamicLightParam ¶m = GetParam(); + + if (param.type == client::DynamicLightTypeSpotlight) { + for (const Plane3 &plane : clipPlanes) { + if (plane.GetDistanceTo(center) < -radius) { + return false; + } + } } + + float maxDistance = radius + param.radius; + return (center - param.origin).GetPoweredLength() < maxDistance * maxDistance; } } } diff --git a/Sources/Draw/GLDynamicLight.h b/Sources/Draw/GLDynamicLight.h index ed4922d8f..40ccb16c7 100644 --- a/Sources/Draw/GLDynamicLight.h +++ b/Sources/Draw/GLDynamicLight.h @@ -21,6 +21,8 @@ #pragma once #include +#include +#include namespace spades { namespace draw { @@ -28,11 +30,18 @@ namespace spades { client::DynamicLightParam param; Matrix4 projMatrix; + /** World-space clip planes (spotlight only) */ + std::array clipPlanes; + public: GLDynamicLight(const client::DynamicLightParam ¶m); const client::DynamicLightParam &GetParam() const { return param; } const Matrix4 &GetProjectionMatrix() const { return projMatrix; } + + bool Cull(const AABB3 &) const; + + bool SphereCull(const Vector3 ¢er, float radius) const; }; } } diff --git a/Sources/Draw/GLDynamicLightShader.cpp b/Sources/Draw/GLDynamicLightShader.cpp index c5d46e002..f21adc351 100644 --- a/Sources/Draw/GLDynamicLightShader.cpp +++ b/Sources/Draw/GLDynamicLightShader.cpp @@ -100,20 +100,5 @@ namespace spades { return texStage; } - - bool GLDynamicLightShader::Cull(const GLDynamicLight &light, const spades::AABB3 &box) { - // TOOD: more tighter check? - // TODO: spotlight check? - // TODO: move this function to GLDynamicLight? - const client::DynamicLightParam ¶m = light.GetParam(); - return box.Inflate(param.radius) && param.origin; - } - - bool GLDynamicLightShader::SphereCull(const GLDynamicLight &light, - const spades::Vector3 ¢er, float radius) { - const client::DynamicLightParam ¶m = light.GetParam(); - float maxDistance = radius + param.radius; - return (center - param.origin).GetPoweredLength() < maxDistance * maxDistance; - } } } diff --git a/Sources/Draw/GLDynamicLightShader.h b/Sources/Draw/GLDynamicLightShader.h index 6c85dd6bc..61c9c3638 100644 --- a/Sources/Draw/GLDynamicLightShader.h +++ b/Sources/Draw/GLDynamicLightShader.h @@ -49,11 +49,6 @@ namespace spades { static std::vector RegisterShader(GLProgramManager *); - static bool Cull(const GLDynamicLight &light, const AABB3 &); - - static bool SphereCull(const GLDynamicLight &light, const Vector3 ¢er, - float radius); - /** setups shadow shader. * note that this function sets the current active texture * stage to the returned texture stage. diff --git a/Sources/Draw/GLMapChunk.cpp b/Sources/Draw/GLMapChunk.cpp index e45ff52c8..a7bef7f14 100644 --- a/Sources/Draw/GLMapChunk.cpp +++ b/Sources/Draw/GLMapChunk.cpp @@ -502,7 +502,7 @@ namespace spades { static GLDynamicLightShader lightShader; lightShader(renderer->renderer, program, lights[i], 1); - if (!GLDynamicLightShader::Cull(lights[i], bx)) + if (!lights[i].Cull(bx)) continue; device->DrawElements(IGLDevice::Triangles, diff --git a/Sources/Draw/GLOptimizedVoxelModel.cpp b/Sources/Draw/GLOptimizedVoxelModel.cpp index c05c01642..3fef74aa3 100644 --- a/Sources/Draw/GLOptimizedVoxelModel.cpp +++ b/Sources/Draw/GLOptimizedVoxelModel.cpp @@ -857,7 +857,7 @@ namespace spades { device->DepthRange(0.f, 0.1f); } for (size_t i = 0; i < lights.size(); i++) { - if (!GLDynamicLightShader::SphereCull(lights[i], param.matrix.GetOrigin(), rad)) + if (!lights[i].SphereCull(param.matrix.GetOrigin(), rad)) continue; dlightShader(renderer, dlightProgram, lights[i], 2); diff --git a/Sources/Draw/GLVoxelModel.cpp b/Sources/Draw/GLVoxelModel.cpp index b05507acf..176608df3 100644 --- a/Sources/Draw/GLVoxelModel.cpp +++ b/Sources/Draw/GLVoxelModel.cpp @@ -567,7 +567,7 @@ namespace spades { device->DepthRange(0.f, 0.1f); } for (size_t i = 0; i < lights.size(); i++) { - if (!GLDynamicLightShader::SphereCull(lights[i], param.matrix.GetOrigin(), rad)) + if (!lights[i].SphereCull(param.matrix.GetOrigin(), rad)) continue; dlightShader(renderer, dlightProgram, lights[i], 0);