From bd10e80d29e17edc39b352e09f63a08a3f6c0fa9 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 3 Dec 2024 18:06:57 -0500 Subject: [PATCH 01/17] Quaternion: add normal-vector constructor - Remove erroneous single-vector constructor, add constructor which computes rotation to a normal vector from some unit vector - Add helper methods to construct a quaternion from up/forward directions --- src/Quaternion.h | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/Quaternion.h b/src/Quaternion.h index c04a6beac4e..d1d4101196d 100644 --- a/src/Quaternion.h +++ b/src/Quaternion.h @@ -4,7 +4,7 @@ #ifndef _QUATERNION_H #define _QUATERNION_H -#include "matrix4x4.h" +#include "matrix3x3.h" #include "vector3.h" #include #include @@ -21,7 +21,7 @@ class Quaternion { Quaternion(); Quaternion(T w, T x, T y, T z); // from angle and axis - Quaternion(T ang, vector3 axis) + Quaternion(T ang, const vector3 &axis) { const T halfAng = ang * T(0.5); const T sinHalfAng = sin(halfAng); @@ -30,14 +30,23 @@ class Quaternion { y = axis.y * sinHalfAng; z = axis.z * sinHalfAng; } - // from axis, assume angle == PI - // optimized fast path using sin(PI/2) = 1 - Quaternion(vector3 axis) + // Create quaternion from normalized direction vectors. + // This creates a quaternion representing the rotation from the second + // unit direction vector to the first unit direction vector. + Quaternion(const vector3 &to, const vector3 &from) { - w = 0; - x = axis.x; - y = axis.y; - z = axis.z; + // Use half-angle trig identities to skip invoking trig functions + T cosAng = to.Dot(from); + // when to and from are equal (colinear), floating point error can cause + // this to become sqrt(-epsilon) without the std::max + T sinHalfAng = sqrt(std::max(1.0 - cosAng, 0.0) / 2.0); + // use the cross product to find the axis of rotation between the two vectors + vector3 rot = from.Cross(to).NormalizedSafe(); + + w = sqrt((1.0 + cosAng) / 2.0); + x = rot.x * sinHalfAng; + y = rot.y * sinHalfAng; + z = rot.z * sinHalfAng; } Quaternion(const Quaternion &o) : w(o.w), @@ -45,6 +54,18 @@ class Quaternion { y(o.y), z(o.z) {} + // Convenience helper + static Quaternion FromUpVector(const vector3 &normal) + { + return Quaternion(normal, vector3(0, 1, 0)); + } + + // Convenience helper + static Quaternion FromForwardVector(const vector3 &normal) + { + return Quaternion(normal, vector3(0, 0, -1)); + } + void GetAxisAngle(T &angle, vector3 &axis) { if (w > 1.0) *this = Normalized(); // if w>1 acos and sqrt will produce errors, this can't happen if quaternion is normalised @@ -106,6 +127,11 @@ class Quaternion { vector3 xyz = vector3(a.x, a.y, a.z); return vec + 2.0 * (vec.Cross(xyz) + a.w * vec).Cross(xyz); } + + // vector * quaternion = inverse multiplication scam + friend vector3 operator*(const vector3 &lhs, const Quaternion &rhs) { return ~rhs * lhs; } + + friend Quaternion operator+(const Quaternion &a, const Quaternion &b) { Quaternion r; From 3dc9d46fe4b709dc2e9c11fa9651aa4c42f2c626 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 3 Dec 2024 20:52:31 -0500 Subject: [PATCH 02/17] ConnectionTicket: add disconnect() function - Simply aliases to the underlying sigc::connection for early cleanup --- src/ConnectionTicket.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ConnectionTicket.h b/src/ConnectionTicket.h index 25db1354a94..a7097438dfc 100644 --- a/src/ConnectionTicket.h +++ b/src/ConnectionTicket.h @@ -19,5 +19,9 @@ struct ConnectionTicket { m_connection = c; } + void disconnect() { + m_connection.disconnect(); + } + sigc::connection m_connection; }; From 02e34c9423ed19d3e1ea2883a03987c05536c61c Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 00:09:30 -0500 Subject: [PATCH 03/17] Add StringName overloads to LuaPushPull --- src/lua/LuaPushPull.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lua/LuaPushPull.h b/src/lua/LuaPushPull.h index cb6cee4279d..912daa4919d 100644 --- a/src/lua/LuaPushPull.h +++ b/src/lua/LuaPushPull.h @@ -5,6 +5,7 @@ #define _LUAPUSHPULL_H #include "Lua.h" +#include "core/StringName.h" #include #include @@ -28,6 +29,10 @@ inline void pi_lua_generic_push(lua_State *l, const std::string &value) { lua_pushlstring(l, value.c_str(), value.size()); } +inline void pi_lua_generic_push(lua_State *l, const StringName &value) +{ + lua_pushlstring(l, value.c_str(), value.size()); +} inline void pi_lua_generic_push(lua_State *l, const std::string_view &value) { lua_pushlstring(l, value.data(), value.size()); @@ -64,6 +69,12 @@ inline void pi_lua_generic_pull(lua_State *l, int index, std::string &out) const char *buf = luaL_checklstring(l, index, &len); std::string(buf, len).swap(out); } +inline void pi_lua_generic_pull(lua_State *l, int index, StringName &out) +{ + size_t len; + const char *buf = luaL_checklstring(l, index, &len); + out = StringName(std::string_view(buf, len)); +} // the returned string view is only valid until the lua object is removed from the stack inline void pi_lua_generic_pull(lua_State *l, int index, std::string_view &out) { From 3cb788f3ae9a7ae45a31c0e7ff7667c3c57fae76 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 00:09:40 -0500 Subject: [PATCH 04/17] LuaVector2: add vector2f push/pull overloads --- src/lua/LuaVector2.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lua/LuaVector2.h b/src/lua/LuaVector2.h index 9d5cf7fc45d..0c03586556c 100644 --- a/src/lua/LuaVector2.h +++ b/src/lua/LuaVector2.h @@ -39,4 +39,21 @@ inline bool pi_lua_strict_pull(lua_State *l, int index, vector2d &out) return false; } +inline void pi_lua_generic_push(lua_State *l, const vector2f &value) { LuaVector2::PushToLua(l, vector2d(value)); } + +inline void pi_lua_generic_pull(lua_State *l, int index, vector2f &out) +{ + out = vector2f(*LuaVector2::CheckFromLua(l, index)); +} + +inline bool pi_lua_strict_pull(lua_State *l, int index, vector2f &out) +{ + const vector2d *tmp = LuaVector2::GetFromLua(l, index); + if (tmp) { + out = vector2f(*tmp); + return true; + } + return false; +} + #endif From ab67426e88b1992c8f22a93b4e774a93f7d251e1 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Tue, 3 Dec 2024 23:13:47 -0500 Subject: [PATCH 05/17] Add GunManager implementation - Responsible for managing all "permanent" weapons (i.e. not missiles) attached to a ship. - Stores all associated data about a ship's hull configuration and mounted weapons directly, due to a lack of support for serialization of LuaSharedObject. - Each weapon is assigned to a single group, which is responsible for sending fire commands to that weapon and managing targeting. - Originally written (but not merged) in 2021. - Future expansion is intended to support more flexible damage types, explosive projectiles, and dumbfire rocket launchers. - This system is intended to handle turrets in the future, via extensions to the weapon mounting system to support multiple traverse options. GunManager: add FiredThisFrame/StoppedThisFrame - Supports our extremely hacky sound code system - Ideally a "better" solution would be found at some point, but this is adequate for the time being GunManager: add GetGroupLeadPos - Temporary accessor that sums together the lead positions of all weapons in the group to support indicator rendering GunManager: add GetGroupTemperatureState - Temporary API to query the average temperature of all weapons in the group relative to their overheat threshold --- src/ship/GunManager.cpp | 519 ++++++++++++++++++++++++++++++++++++++++ src/ship/GunManager.h | 222 +++++++++++++++++ 2 files changed, 741 insertions(+) create mode 100644 src/ship/GunManager.cpp create mode 100644 src/ship/GunManager.h diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp new file mode 100644 index 00000000000..a6463627a5a --- /dev/null +++ b/src/ship/GunManager.cpp @@ -0,0 +1,519 @@ +// Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "GunManager.h" + +#include "Beam.h" +#include "Projectile.h" +#include "Game.h" +#include "ModelBody.h" +#include "Pi.h" +#include "core/Log.h" +#include "lua/LuaBodyComponent.h" +#include "matrix4x4.h" +#include "scenegraph/Tag.h" + +#include "imgui/imgui.h" + +REGISTER_COMPONENT_TYPE(GunManager) { + BodyComponentDB::RegisterComponent("GunManager"); + BodyComponentDB::RegisterSerializer(); + BodyComponentDB::RegisterLuaInterface(); +} + +void GunManager::Init(ModelBody *b) +{ + m_parent = b; + + // Ensure we always have a default group for weapons + if (m_groups.empty()) { + m_groups.push_back({}); + } +} + +void GunManager::SaveToJson(Json &jsonObj, Space *space) +{ +} + +void GunManager::LoadFromJson(const Json &jsonObj, Space *space) +{ +} + +void GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimit) +{ + WeaponMount mount = {}; + + mount.id = id; + mount.tag = m_parent->GetModel()->FindTagByName(tagName); + mount.gimbalLimitTan = vector2f( + tan(DEG2RAD(gimbalLimit.x)), + tan(DEG2RAD(gimbalLimit.y)) + ); + + m_mounts.try_emplace(id, mount); +} + +void GunManager::RemoveWeaponMount(StringName id) +{ + auto iter = m_mounts.find(id); + if (iter == m_mounts.end()) + return; + + if (IsWeaponMounted(id)) + return; + + m_mounts.erase(iter); +} + +bool GunManager::MountWeapon(StringName hardpoint, const WeaponData &gunData) +{ + auto iter = m_mounts.find(hardpoint); + if (iter == m_mounts.end()) + return false; + + if (IsWeaponMounted(hardpoint)) + return false; + + WeaponState ws = {}; + + ws.mount = &iter->second; + ws.data = gunData; + + // TODO: render weapon models on hardpoint + // if (!gunData.modelPath.empty()) { + // ws.model = Pi::FindModel(gunData.modelPath); + // } + + // assign the new weapon to group zero + // TODO: make a new group for this weapon? + m_groups.front().weapons[m_weapons.size()] = true; + m_weapons.push_back(ws); + + return true; +} + +void GunManager::UnmountWeapon(StringName hardpoint) +{ + auto iter = std::find_if(m_weapons.begin(), m_weapons.end(), [&](const WeaponState &ws) { + return ws.mount->id == hardpoint; + }); + + if (iter != m_weapons.end()) { + uint32_t index = std::distance(m_weapons.begin(), iter); + + // Update all group weapon assignments + for (auto &group : m_groups) { + RemoveGroupIndex(group.weapons, index); + } + + // TODO: any additional cleanup needed for other systems that hook into GunManager, e.g. heat, targeting, etc. + m_weapons.erase(iter); + } +} + +bool GunManager::IsWeaponMounted(StringName hardpoint) const +{ + return std::find_if(m_weapons.begin(), m_weapons.end(), [&](const WeaponState &ws) { + return ws.mount->id == hardpoint; + }) != m_weapons.end(); +} + +void GunManager::RemoveGroupIndex(WeaponIndexSet &group, uint32_t index) +{ + // Mask containing all of the indices below index + WeaponIndexSet keep = WeaponIndexSet().set() >> (NUM_WEAPON_INDICES - index); + // Mask containing all of the indices above index + WeaponIndexSet mask = WeaponIndexSet().set() << (index + 1); + + // Decrement all contained indices in the set above the removed index down by one + group = (group & keep) | ((group & mask) >> 1); +} + +uint32_t GunManager::GetWeaponIndexForHardpoint(StringName hardpoint) const +{ + auto iter = std::find_if(m_weapons.begin(), m_weapons.end(), [&](const WeaponState &ws) { + return ws.mount->id == hardpoint; + }); + + if (iter == m_weapons.end()) { + return UINT32_MAX; + } + + return std::distance(m_weapons.begin(), iter); +} + +void GunManager::SetupDefaultGroups() +{ + m_groups.clear(); + + GroupState group = {}; + + for (size_t i = 0; i < m_weapons.size(); i++) { + group.weapons[i] = true; + } + + m_groups.push_back(group); +} + +void GunManager::SetAllGroupsFiring(bool firing) +{ + for (size_t idx = 0; idx < m_groups.size(); idx++) { + SetGroupFiring(idx, firing); + } + + m_isAnyFiring = firing; +} + +void GunManager::AssignWeaponToGroup(uint32_t numWeapon, uint32_t group) +{ + if (numWeapon > m_weapons.size()) + return; + + if (group >= m_groups.size()) { + m_groups.resize(group + 1); + } + + WeaponState &ws = m_weapons[numWeapon]; + + uint32_t oldGroup = ws.group; + m_groups[oldGroup].weapons[numWeapon] = false; + + // Prune empty groups when the last weapon is removed + if (m_groups[oldGroup].weapons.none()) { + RemoveGroup(oldGroup); + } + + ws.group = group; + m_groups[group].weapons[numWeapon] = true; +} + +void GunManager::RemoveGroup(uint32_t group) +{ + if (group == 0 || group >= m_groups.size()) + return; + + GroupState &gzero = m_groups[0]; + + for (size_t idx = 0; idx < m_weapons.size(); idx++) { + WeaponState &weapon = m_weapons[idx]; + + // Reassign weapons from this group to group zero + if (weapon.group == group) { + weapon.group = 0; + gzero.weapons[idx] = true; + } + + // Reduce index of all groups above this + if (weapon.group > group) { + weapon.group--; + } + } + + // Finally, clear the group from the list + m_groups.erase(m_groups.begin() + group); +} + +const Body *GunManager::GetGroupTarget(uint32_t group) +{ + if (group >= m_groups.size()) + return nullptr; + + return m_groups[group].target; +} + +void GunManager::SetGroupTarget(uint32_t group, const Body *target) +{ + if (group >= m_groups.size()) + return; + + GroupState &gs = m_groups[group]; + // No weapons at all, ignore + if (!gs.weapons.any()) + return; + + // Clear the prior callback, if any + if (gs.target) + gs.onTargetDestroyed.disconnect(); + + m_groups[group].target = target; + + if (target) { + gs.onTargetDestroyed = target->onDelete.connect([=]() { + this->SetGroupTarget(group, nullptr); + }); + } +} + +void GunManager::SetGroupFiring(uint32_t group, bool firing) +{ + if (group >= m_groups.size()) + return; + + GroupState &gs = m_groups[group]; + // No weapons at all, ignore + if (!gs.weapons.any()) + return; + + gs.firing = firing; + m_isAnyFiring |= firing; + + if (firing) { + // Update the next-firing time for all weapons in this group so they actually fire + // (Ensures you can't spam the fire command to shoot more rapidly than the weapon's rate of fire) + for (size_t idx = 0; idx < m_weapons.size(); idx++) { + if (gs.weapons[idx]) { + m_weapons[idx].nextFireTime = std::max(m_weapons[idx].nextFireTime, Pi::game->GetTime()); + } + } + } else { + m_stoppedNextFrame |= gs.weapons; + } +} + +void GunManager::SetGroupFireWithoutTargeting(uint32_t group, bool enabled) +{ + if (group >= m_groups.size()) + return; + + GroupState &gs = m_groups[group]; + gs.fireWithoutTargeting = enabled; +} + +vector3d GunManager::GetGroupLeadPos(uint32_t group) +{ + if (group >= m_groups.size() || !m_groups[group].target || m_groups[group].weapons.none()) + return vector3d(0, 0, 0); + + GroupState &gs = m_groups[group]; + double inv_count = 1.0 / double(gs.weapons.count()); + vector3d lead_pos = vector3d(0, 0, 0); + + for (size_t idx = 0; idx < m_weapons.size(); idx++) { + if (gs.weapons[idx]) { + lead_pos += m_weapons[idx].currentLeadPos * inv_count; + } + } + + return lead_pos; +} + +float GunManager::GetGroupTemperatureState(uint32_t group) +{ + if (group >= m_groups.size() || m_groups[group].weapons.none()) + return 0.f; + + GroupState &gs = m_groups[group]; + double inv_count = 1.0 / double(gs.weapons.count()); + float temperature = 0.f; + + for (size_t idx = 0; idx < m_weapons.size(); idx++) { + if (gs.weapons[idx]) { + temperature += inv_count * (m_weapons[idx].temperature / m_weapons[idx].data.overheatThreshold); + } + } + + return temperature; +} + +void GunManager::StaticUpdate(float deltaTime) +{ + bool isAnyFiring = false; + + static ImGuiOnceUponAFrame thisFrame; + bool shouldDrawDebug = m_parent && m_parent->IsType(ObjectType::PLAYER) && thisFrame; + if (shouldDrawDebug) + ImGui::Begin("GunMangerDebug", nullptr, ImGuiWindowFlags_NoFocusOnAppearing); + + if (shouldDrawDebug) { + ImGui::Text("Gun mounts: %ld, mounted: %ld", m_mounts.size(), m_weapons.size()); + ImGui::Spacing(); + } + + m_stoppedThisFrame = m_stoppedNextFrame; + m_stoppedNextFrame.reset(); + m_firedThisFrame.reset(); + + // TODO(sensors): go through groups and collate sensor information on group target + + for (WeaponState &gun : m_weapons) { + + GroupState &gs = m_groups[gun.group]; + + // REMOVE: Draw debug state + if (shouldDrawDebug) { + ImGui::Text("== Gun Mount: %s, target %s", gun.mount->id.c_str(), gs.target ? gs.target->GetLabel().c_str() : ""); + ImGui::Indent(); + ImGui::Text("Temperature: %f", gun.temperature); + ImGui::Text("Firing: %d", gs.firing); + ImGui::Text("Damage: %f", gun.data.projectile.damage); + ImGui::Text("NextFireTime: %f", gun.nextFireTime); + ImGui::Text("Current Time: %f", Pi::game->GetTime()); + ImGui::Text("Firing RPM: %f", gun.data.firingRPM); + ImGui::Text("Firing Heat: %f", gun.data.firingHeat); + ImGui::Text("Overheat At: %f", gun.data.overheatThreshold); + ImGui::Text("Beam: %d", gun.data.projectile.beam); + ImGui::Text("Mining: %d", gun.data.projectile.mining); + const auto &leadDir = gun.currentLead; + ImGui::Text("CurrentLeadDir: %f, %f, %f", leadDir.x, leadDir.y, leadDir.z); + ImGui::Unindent(); + ImGui::Spacing(); + } + + bool isBeam = gun.data.projectile.beam || gun.data.projectileType == PROJECTILE_BEAM; + + // Compute weapon lead direction + if (gs.target) { + const matrix3x3d &orient = m_parent->GetOrient(); + const vector3d relPosition = gs.target->GetPositionRelTo(m_parent); + const vector3d relVelocity = gs.target->GetVelocityRelTo(m_parent->GetFrame()) - m_parent->GetVelocity(); + + // bring velocity and acceleration into ship-space + CalcWeaponLead(&gun, relPosition * orient, relVelocity * orient); + } else { + gun.currentLead = vector3f(0, 0, 1); + gun.currentLeadPos = vector3d(0, 0, 0); + } + + // Update gun cooling per tick + gun.temperature = std::max(0.0f, gun.temperature - gun.data.coolingPerSecond * m_coolingBoost * deltaTime); + double currentTime = Pi::game->GetTime(); + + // determine if we should fire this update + isAnyFiring |= gs.firing; + + // determine if something prevents this gun from firing + // Temperature, gimbal checks, etc. + bool canFire = gun.temperature < gun.data.overheatThreshold && (gs.fireWithoutTargeting || gs.target && gun.withinGimbalLimit); + + if (gs.firing && currentTime >= gun.nextFireTime) { + + // Determine how much time we missed since the gun was supposed to fire + double missedTime = currentTime - gun.nextFireTime; + // time between shots, used to determine how many shots we need to 'catch up' on this timestep + double deltaShot = 60.0 / gun.data.firingRPM; + // only fire multiple shots per timestep if the accumulated error and the length of the timestep require it + // given that timescale is set to 1 while in combat, this is likely not going to be required except for NPCs + uint32_t numShots = 1 + floor((missedTime + deltaTime) / deltaShot); + + for (uint32_t i = 0; i < numShots; ++i) { + Fire(&gun, &gs); + } + + // set the next fire time, making sure to preserve accumulated (fractional) shot time + gun.nextFireTime += deltaShot * numShots; + + size_t weaponIdx = std::distance(&m_weapons.front(), &gun); + m_firedThisFrame[weaponIdx] = true; + + } + + // ensure next fire time is properly handled when the gun is meant to be firing + // but unable to fire (e.g. during gun overheat) + if (gs.firing) + gun.nextFireTime = std::max(gun.nextFireTime, currentTime); + + } + + m_isAnyFiring = isAnyFiring; + + if (shouldDrawDebug) + ImGui::End(); +} + +void GunManager::Fire(WeaponState *weapon, GroupState *group) +{ + WeaponData *data = &weapon->data; + + // either fire the next barrel in sequence or fire all at the same time + size_t firstBarrel = 0; + size_t numBarrels = 1; + if (data->staggerBarrels || data->numBarrels == 1) { + firstBarrel = (weapon->lastBarrel + 1) % data->numBarrels; + weapon->lastBarrel = firstBarrel; + } else { + numBarrels = data->numBarrels; + } + + const matrix4x4f &xform = GetMountTransform(weapon); + const vector3d wpn_pos = vector3d(xform.GetTranslate()); + const matrix3x3f wpn_orient = xform.GetOrient(); + + const matrix3x3d &orient = m_parent->GetOrient(); + + // mount-relative aiming direction + const vector3d leadDir = vector3d(wpn_orient * weapon->currentLead).Normalized(); + + for (size_t idx = firstBarrel; idx < firstBarrel + numBarrels; idx++) { + weapon->temperature += data->firingHeat; + // TODO: get individual barrel locations from gun model and cache them + const vector3d dir = orient * leadDir; + const vector3d pos = orient * wpn_pos + m_parent->GetPosition(); + + // TODO: projectile system using new ProjectileDef data + if (data->projectile.beam) { + Beam::Add(m_parent, data->projectile, pos, m_parent->GetVelocity(), dir); + } else { + Projectile::Add(m_parent, data->projectile, pos, m_parent->GetVelocity(), data->projectile.speed * dir); + } + } +} + +// Note that position and relative velocity are in the coordinate system of the host body +void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity) +{ + // Compute the forward vector for the weapon mount + const matrix4x4f &xform = GetMountTransform(state); + const vector3f forward = vector3f(0, 0, 1); + + if (state->data.projectileType == PROJECTILE_BALLISTIC) { + // Calculate firing solution and relative velocity along our z axis by + // computing the position along the enemy ship's lead vector at which to aim + const double projspeed = state->data.projectile.speed; + // Account for the distance between the weapon mount and the center of the parent + position -= vector3d(xform.GetTranslate()); + + // 1. First, generate a basic approximation to estimate travel time + vector3d leadpos = position + relativeVelocity * (position.Length() / projspeed); + // 2. This second-order approximation yields a much more accurate estimate + float travelTime = leadpos.Length() / projspeed; + leadpos = position + relativeVelocity * travelTime; + // A third-order approximation here would need to take into account the target's + // expected acceleration over the duration by using a moving average of acceleration + // rather than taking the instantaneous forces acting on the body. + // Thus, we refrain from implementing it for now. + + state->currentLeadPos = leadpos; + } else if (state->data.projectileType == PROJECTILE_BEAM) { + // Beam weapons should just aim at the target + state->currentLeadPos = position; + } + + // Transform the target's direction into the coordinate space of the mount, + // with the barrel pointing "forward" towards +Z. + // float has plenty of precision when working with normalized directions. + vector3f targetDir = vector3f(state->currentLeadPos.Normalized()) * xform.GetOrient(); + + // We represent the maximum traverse of the weapon as an ellipse relative + // to the +Z axis of the mount. + // To determine whether the lead target is within this traverse, we modify + // the coordinate system such that the ellipse becomes the unit circle in + // 2D space, and test the length of the 2D components of the direction + // vector. + // Note that we scale the targetDir vector such that the z component has a length of 1.0, + // so that the comparison with the tangent of the gimbal limit is correct. + vector2f traverseRel = (targetDir * (1.0 / targetDir.z)).xy() / state->mount->gimbalLimitTan; + + state->withinGimbalLimit = targetDir.z > 0 && traverseRel.LengthSqr() <= 1.0; + state->currentLead = state->withinGimbalLimit ? targetDir : forward; +} + +static const matrix4x4f s_noMountTransform = matrix4x4f::RotateXMatrix(M_PI); + +const matrix4x4f &GunManager::GetMountTransform(WeaponState *state) +{ + if (state->mount->tag) { + return state->mount->tag->GetGlobalTransform(); + } + + return s_noMountTransform; +} diff --git a/src/ship/GunManager.h b/src/ship/GunManager.h new file mode 100644 index 00000000000..98b5caa4ec2 --- /dev/null +++ b/src/ship/GunManager.h @@ -0,0 +1,222 @@ +// Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "ConnectionTicket.h" +#include "Projectile.h" +#include "core/StringName.h" +#include "lua/LuaWrappable.h" +#include "scenegraph/Model.h" +#include +#include + +class ModelBody; + +/** + * Class: GunManager + * + * The GunManager is responsible for managing all combat-related aspects of ship + * weaponry. + * + * The typical lifecycle of the GunManager component is as follows: + * - A GunManager is constructed when a ship is created or its type changed + * - It is informed about the available weapon mounts on a ship shortly after construction + * - Weapons are attached to those mounts by the equipment code + * - Weapons are assigned group indices to allow separate fire control and targeting + * - Targets are assigned to individual groups + * - A higher-level input manager sends fire commands for each group + * - Weapon mounts are added or removed as needed by equipment attached to the ship + * - Finally, when the ship is destroyed or its type changed, the GunManager is destroyed + */ +class GunManager : public LuaWrappable { +public: + static constexpr size_t NUM_WEAPON_INDICES = 256; + using WeaponIndexSet = std::bitset; + + struct WeaponMount; + + enum ProjectileType : uint8_t { // + PROJECTILE_BALLISTIC, // Impact weapons: pulsecannons, railguns, etc. + PROJECTILE_BEAM, // Instantaneous-hit weapons: lasers + }; + + struct ProjectileDef { + float lifespan = 5.0; + float speed = 1000.0; + + // Damage + float impactDamage = 0.0; // How much damage is dealt when this projectile hits a target + float explosiveDamage = 0.0; // When this projectile detonates (impact or proximity fuse) how much damage is dealt in the radius? + float explosionRadius = 0.0; // How large of an explosion radius does this projectile have? + float explosionFalloff = 1.0; // How does the damage fall off from the center to the edge of the explosion? 2.0 = quadratic, 1.0 = linear, 0.0 = no falloff, use "impact area" approximation instead + // TODO: damage type + + // Feature flags + uint8_t isMining : 1; // Does this projectile deal "mining" damage? TODO: better damage type system + uint8_t hasProxyFuse : 1; // Does this projectile have a proximity fuse? + uint8_t proxyIFF : 1; // Is the proximity fuse capable of reading IFF data? + + // Fusing + float proxyFuseRadius = 0.0; // This projectile will detonate when it detects a body within this range + float proxyFuseArmTime = 0.5; // How long after firing before the projectile fuse is armed? + + // Visual settings + float length = 0; + float width = 0; + Color color; + }; + + // Information about a specific "weapon type" + // TODO: create one of these in Lua per weapon definition and reference them from each mounted gun + // TODO: create a separate projectile definition and reference it + struct WeaponData { + float firingRPM = 240; // number of shots per minute (60s / time between shots) + float firingHeat = 4.5; // amount of thermal energy(kJ) emitted into the system per shot + + //TODO: integrate this with a whole-ship cooling system + float coolingPerSecond = 14.2; // nominal amount of thermal energy removed per second (kW) + float overheatThreshold = 280; // total amount of thermal energy(kJ) the gun can store while functioning + + std::string modelPath; // model to render this weapon with + + ProjectileData projectile; // deprecated, to replace with RefCountedPtr + ProjectileType projectileType = PROJECTILE_BALLISTIC; + uint8_t numBarrels = 1; // total number of barrels on the model + bool staggerBarrels = false; // should we fire one barrel after another, or both at the same time? + }; + + // Information about a specific mounted weapon (serialized directly) + struct WeaponState { + WeaponMount *mount; // Mounted hardpoint + uint8_t group = 0; // Group this hardpoint belongs to + bool withinGimbalLimit = false; // does the gun have a valid firing solution on the target? + + uint8_t lastBarrel = 0; // the last barrel used to fire (for multi-barrel weapons) + float temperature = 0; // current gun temperature + double nextFireTime = 0; // time at which the gun will be ready to fire again + // TODO: integrate this with a whole-ship cooling system + // Currently uses GunManager::m_coolingBoost + // float coolerOverclock = 1.0; // any boost to gun coolers from ship equipment + // float powerOverclock = 1.0; // boost to overall firerate (and corresponding increase in heat generation) + + vector3f currentLead; + vector3d currentLeadPos; + + WeaponData data; + SceneGraph::Model *model; // gun model, currently unused + }; + + struct WeaponMount { + StringName id; + SceneGraph::Tag *tag; // Tag in the parent model that this weapon mount is attached to + vector2f gimbalLimitTan; // tangent of gimbal limits + //SceneGraph::Model *model; // model to render for this weapon mount, if any + // TODO: enable/disable hardpoint based on ship configuration, i.e. landing/vtol/wings? + }; + + struct GroupState { + WeaponIndexSet weapons; // Whic weapons are assigned to this group? + const Body *target; // The target for this group, if any + bool firing; // Is the group currently firing + bool fireWithoutTargeting; // Can the group fire without a target/target not in gimbal range? + ConnectionTicket onTargetDestroyed; // Unlock the target once it's been destroyed + }; + + GunManager() = default; + + void Init(ModelBody *b); + + void SaveToJson(Json &jsonObj, Space *space); + void LoadFromJson(const Json &jsonObj, Space *space); + + void StaticUpdate(float deltaTime); + + // ========================================== + + // Add a weapon mount to this gun manager + void AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimitDegrees); + // Remove a weapon mount from this gun manager. This will fail if a weapon is still mounted. + void RemoveWeaponMount(StringName id); + + // Attach a weapon to a specific mount + bool MountWeapon(StringName hardpoint, const WeaponData &data); + // Remove the attached weapon from a specific mount + void UnmountWeapon(StringName hardpoint); + // Check if a weapon is attached to a specific mount + bool IsWeaponMounted(StringName hardpoint) const; + + const std::vector &GetWeapons() const { return m_weapons; } + + uint32_t GetNumWeapons() const { return m_weapons.size(); } + const WeaponState *GetWeaponState(uint32_t numWeapon) const { return m_weapons.size() > numWeapon ? &m_weapons[numWeapon] : nullptr; } + uint32_t GetWeaponIndexForHardpoint(StringName hardpoint) const; + + // ========================================== + + // For AI/NPC ship usage, combines all weapons into a single group + void SetupDefaultGroups(); + void SetTrackingTarget(const Body *target) { return SetGroupTarget(0, target); } + void SetAllGroupsFiring(bool firing = false); + + // For player weapon management + + // Assign a specific weapon to a firing group + // Weapons are by default all assigned to group 0 + void AssignWeaponToGroup(uint32_t numWeapon, uint32_t group); + // Remove a weapon group in its entirety + void RemoveGroup(uint32_t group); + + const std::vector &GetWeaponGroups() const { return m_groups; } + + const Body *GetGroupTarget(uint32_t group); + void SetGroupTarget(uint32_t group, const Body *target); + void SetGroupFiring(uint32_t group, bool firing); + // Sets whether the group will authorize individual weapons to fire without a target within the gimbal arc of the weapon + void SetGroupFireWithoutTargeting(uint32_t group, bool enabled); + + // TEMP: return the average lead position of a firing group - should query individual weapons instead + vector3d GetGroupLeadPos(uint32_t group); + // TEMP: return the average temperature state of a weapon group in 0..1 + float GetGroupTemperatureState(uint32_t group); + + // ========================================== + + bool IsFiring() const { return m_isAnyFiring; } + bool IsGroupFiring(uint32_t group) const { return m_groups.size() > group ? m_groups[group].firing : false; } + + // TODO: separate this functionality to a ship-wide cooling system + void SetCoolingBoost(float boost) { m_coolingBoost = boost; } + + // Semi-hacky, would like to replace with a better eventing system of some sort + WeaponIndexSet GetGunsFiredThisFrame() const { return m_firedThisFrame; } + WeaponIndexSet GetGunsStoppedThisFrame() const { return m_stoppedThisFrame; } + +private: + + // Handle checking and firing a given gun. + // Note that this currently does not nicely handle spawning multiple projectiles per timestep - i.e. timewarp or a weapon RPM higher than 3600 + // Projectile spawns are also "snapped" to the start of a timestep if they are not direct multiples of the timestep duration + void Fire(WeaponState *weapon, GroupState *group); + + // Calculate the position a given gun should aim at to hit the current target body + // This is effectively the position of the target at T+n + void CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity); + + const matrix4x4f &GetMountTransform(WeaponState *weapon); + + void RemoveGroupIndex(WeaponIndexSet &group, uint32_t index); + + std::vector m_groups; + std::vector m_weapons; + std::map m_mounts; + + WeaponIndexSet m_firedThisFrame; + WeaponIndexSet m_stoppedThisFrame; + WeaponIndexSet m_stoppedNextFrame; + + ConnectionTicket m_targetDestroyedCallback; + ModelBody *m_parent = nullptr; + bool m_isAnyFiring = false; + float m_coolingBoost = 1.0; +}; From 771dc702107469328b08df00c803a7da4a6a2cb3 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 19:29:41 -0500 Subject: [PATCH 06/17] GunManager: use exact targeting solution - Ported from the FixedGuns implementation. - Thanks to azieba for writing the solution in the first place. - Uses an exact solution to the question of when the projectile will get there, and then an approximation of the ship's acceleration over that time. - Provides quite a bit better probability-of-hit for ballistic weapons --- src/ship/GunManager.cpp | 59 +++++++++++++++++++++++++++++++---------- src/ship/GunManager.h | 2 +- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp index a6463627a5a..7c8c738e3b0 100644 --- a/src/ship/GunManager.cpp +++ b/src/ship/GunManager.cpp @@ -4,6 +4,7 @@ #include "GunManager.h" #include "Beam.h" +#include "DynamicBody.h" #include "Projectile.h" #include "Game.h" #include "ModelBody.h" @@ -366,9 +367,14 @@ void GunManager::StaticUpdate(float deltaTime) const matrix3x3d &orient = m_parent->GetOrient(); const vector3d relPosition = gs.target->GetPositionRelTo(m_parent); const vector3d relVelocity = gs.target->GetVelocityRelTo(m_parent->GetFrame()) - m_parent->GetVelocity(); + vector3d relAccel = vector3d(0, 0, 0); - // bring velocity and acceleration into ship-space - CalcWeaponLead(&gun, relPosition * orient, relVelocity * orient); + if (gs.target->IsType(ObjectType::DYNAMICBODY)) { + relAccel = static_cast(gs.target)->GetLastForce() / gs.target->GetMass(); + } + + // bring position, velocity and acceleration into ship-space + CalcWeaponLead(&gun, relPosition * orient, relVelocity * orient, relAccel * orient); } else { gun.currentLead = vector3f(0, 0, 1); gun.currentLeadPos = vector3d(0, 0, 0); @@ -459,7 +465,7 @@ void GunManager::Fire(WeaponState *weapon, GroupState *group) } // Note that position and relative velocity are in the coordinate system of the host body -void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity) +void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity, vector3d relativeAccel) { // Compute the forward vector for the weapon mount const matrix4x4f &xform = GetMountTransform(state); @@ -472,17 +478,42 @@ void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d // Account for the distance between the weapon mount and the center of the parent position -= vector3d(xform.GetTranslate()); - // 1. First, generate a basic approximation to estimate travel time - vector3d leadpos = position + relativeVelocity * (position.Length() / projspeed); - // 2. This second-order approximation yields a much more accurate estimate - float travelTime = leadpos.Length() / projspeed; - leadpos = position + relativeVelocity * travelTime; - // A third-order approximation here would need to take into account the target's - // expected acceleration over the duration by using a moving average of acceleration - // rather than taking the instantaneous forces acting on the body. - // Thus, we refrain from implementing it for now. - - state->currentLeadPos = leadpos; + //Exact lead calculation. We start with: + // |targpos * l + targvel| = projspeed + //we solve for l which can be interpreted as 1/time for the projectile to reach the target + //it gives: + // |targpos|^2 * l^2 + targpos*targvel * 2l + |targvel|^2 - projspeed^2 = 0; + // so it gives scalar quadratic equation with two possible solutions - we care only about the positive one - shooting forward + // A basic math for solving, there is probably more elegant and efficient way to do this: + double a = position.LengthSqr(); + double b = position.Dot(relativeVelocity) * 2; + double c = relativeVelocity.LengthSqr() - projspeed * projspeed; + double delta = b * b - 4 * a * c; + + vector3d leadPos = position; + + if (delta >= 0) { + //l = (-b + sqrt(delta)) / 2a; t=1/l; a>0 + double t = 2 * a / (-b + sqrt(delta)); + + if (t < 0 || t > state->data.projectile.lifespan) { + //no positive solution or target too far + } else { + //This is an exact solution as opposed to 2 step approximation used before. + //It does not improve the accuracy as expected though. + //If the target is accelerating and is far enough then this aim assist will + //actually make sure that it is mpossible to hit.. + leadPos = position + relativeVelocity * t; + + //lets try to adjust for acceleration of the target ship + //s=a*t^2/2 -> hitting steadily accelerating ships works at much greater distance + leadPos += relativeAccel * t * t * 0.5; + } + } else { + //no solution + } + + state->currentLeadPos = leadPos; } else if (state->data.projectileType == PROJECTILE_BEAM) { // Beam weapons should just aim at the target state->currentLeadPos = position; diff --git a/src/ship/GunManager.h b/src/ship/GunManager.h index 98b5caa4ec2..2e98a6f4b4f 100644 --- a/src/ship/GunManager.h +++ b/src/ship/GunManager.h @@ -201,7 +201,7 @@ class GunManager : public LuaWrappable { // Calculate the position a given gun should aim at to hit the current target body // This is effectively the position of the target at T+n - void CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity); + void CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity, vector3d relativeAccel); const matrix4x4f &GetMountTransform(WeaponState *weapon); From 661338b033ea2b5250b1d1ecd9ddebd77d5a1881 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 21:00:03 -0500 Subject: [PATCH 07/17] GunManager: remove debug information - Kept as a separate commit for porting to a future body inspector tool for the PerfInfo window --- src/ship/GunManager.cpp | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp index 7c8c738e3b0..acbae23e2ba 100644 --- a/src/ship/GunManager.cpp +++ b/src/ship/GunManager.cpp @@ -9,13 +9,10 @@ #include "Game.h" #include "ModelBody.h" #include "Pi.h" -#include "core/Log.h" #include "lua/LuaBodyComponent.h" #include "matrix4x4.h" #include "scenegraph/Tag.h" -#include "imgui/imgui.h" - REGISTER_COMPONENT_TYPE(GunManager) { BodyComponentDB::RegisterComponent("GunManager"); BodyComponentDB::RegisterSerializer(); @@ -320,16 +317,6 @@ void GunManager::StaticUpdate(float deltaTime) { bool isAnyFiring = false; - static ImGuiOnceUponAFrame thisFrame; - bool shouldDrawDebug = m_parent && m_parent->IsType(ObjectType::PLAYER) && thisFrame; - if (shouldDrawDebug) - ImGui::Begin("GunMangerDebug", nullptr, ImGuiWindowFlags_NoFocusOnAppearing); - - if (shouldDrawDebug) { - ImGui::Text("Gun mounts: %ld, mounted: %ld", m_mounts.size(), m_weapons.size()); - ImGui::Spacing(); - } - m_stoppedThisFrame = m_stoppedNextFrame; m_stoppedNextFrame.reset(); m_firedThisFrame.reset(); @@ -340,26 +327,6 @@ void GunManager::StaticUpdate(float deltaTime) GroupState &gs = m_groups[gun.group]; - // REMOVE: Draw debug state - if (shouldDrawDebug) { - ImGui::Text("== Gun Mount: %s, target %s", gun.mount->id.c_str(), gs.target ? gs.target->GetLabel().c_str() : ""); - ImGui::Indent(); - ImGui::Text("Temperature: %f", gun.temperature); - ImGui::Text("Firing: %d", gs.firing); - ImGui::Text("Damage: %f", gun.data.projectile.damage); - ImGui::Text("NextFireTime: %f", gun.nextFireTime); - ImGui::Text("Current Time: %f", Pi::game->GetTime()); - ImGui::Text("Firing RPM: %f", gun.data.firingRPM); - ImGui::Text("Firing Heat: %f", gun.data.firingHeat); - ImGui::Text("Overheat At: %f", gun.data.overheatThreshold); - ImGui::Text("Beam: %d", gun.data.projectile.beam); - ImGui::Text("Mining: %d", gun.data.projectile.mining); - const auto &leadDir = gun.currentLead; - ImGui::Text("CurrentLeadDir: %f, %f, %f", leadDir.x, leadDir.y, leadDir.z); - ImGui::Unindent(); - ImGui::Spacing(); - } - bool isBeam = gun.data.projectile.beam || gun.data.projectileType == PROJECTILE_BEAM; // Compute weapon lead direction @@ -421,9 +388,6 @@ void GunManager::StaticUpdate(float deltaTime) } m_isAnyFiring = isAnyFiring; - - if (shouldDrawDebug) - ImGui::End(); } void GunManager::Fire(WeaponState *weapon, GroupState *group) From c06dc38451af8fc06d08cbed12d4603da5d93318 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 01:03:03 -0500 Subject: [PATCH 08/17] Add GunManager Lua interface --- data/meta/Components/GunManager.meta.lua | 30 ++++++++++ src/lua/Lua.cpp | 2 + src/lua/LuaGunManager.cpp | 74 ++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 data/meta/Components/GunManager.meta.lua create mode 100644 src/lua/LuaGunManager.cpp diff --git a/data/meta/Components/GunManager.meta.lua b/data/meta/Components/GunManager.meta.lua new file mode 100644 index 00000000000..d6f107330ba --- /dev/null +++ b/data/meta/Components/GunManager.meta.lua @@ -0,0 +1,30 @@ +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +-- This file implements type information about C++ classes for Lua static analysis + +---@meta + +---@class GunManager +local GunManager = {} + +---@param id string +---@param tag string +---@param gimbal Vector2 +function GunManager:AddWeaponMount(id, tag, gimbal) end + +---@param id string +function GunManager:RemoveWeaponMount(id) end + +---@param mount string +---@param weaponData table +---@return boolean +function GunManager:MountWeapon(mount, weaponData) end + +---@param mount string +function GunManager:UnmountWeapon(mount) end + +---@param mount string +function GunManager:IsWeaponMounted(mount) end + +-- TODO... diff --git a/src/lua/Lua.cpp b/src/lua/Lua.cpp index 8647337a35c..2abfee16227 100644 --- a/src/lua/Lua.cpp +++ b/src/lua/Lua.cpp @@ -97,6 +97,8 @@ namespace Lua { LuaObject::RegisterClass(); LuaObject::RegisterClass(); + LuaObject::RegisterClass(); + Pi::luaSerializer = new LuaSerializer(); Pi::luaTimer = new LuaTimer(); diff --git a/src/lua/LuaGunManager.cpp b/src/lua/LuaGunManager.cpp new file mode 100644 index 00000000000..142aad1302d --- /dev/null +++ b/src/lua/LuaGunManager.cpp @@ -0,0 +1,74 @@ +// Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "LuaMetaType.h" +#include "LuaObject.h" +#include "LuaTable.h" +#include "LuaColor.h" +#include "LuaVector2.h" +#include "ship/GunManager.h" + +void pi_lua_generic_pull(lua_State *l, int idx, ProjectileData &out) +{ + luaL_checktype(l, idx, LUA_TTABLE); + LuaTable tab(l, idx); + + out.damage = tab.Get("damage"); + out.lifespan = tab.Get("lifespan"); + out.speed = tab.Get("speed"); + + out.beam = tab.Get("beam"); + out.mining = tab.Get("mining"); + + out.length = tab.Get("length"); + out.width = tab.Get("width"); + out.color = tab.Get("color"); +} + +void pi_lua_generic_pull(lua_State *l, int idx, GunManager::WeaponData &out) +{ + luaL_checktype(l, idx, LUA_TTABLE); + LuaTable tab(l, idx); + + out.firingRPM = tab.Get("rpm"); + out.firingHeat = tab.Get("heatPerShot"); + out.coolingPerSecond = tab.Get("cooling"); + out.overheatThreshold = tab.Get("overheat"); + out.modelPath = tab.Get("model", ""); + out.projectile = tab.Get("projectile"); + out.projectileType = out.projectile.beam ? GunManager::PROJECTILE_BEAM : GunManager::PROJECTILE_BALLISTIC; + out.numBarrels = tab.Get("numBarrels", 1); + out.staggerBarrels = tab.Get("stagger", true); +} + +template<> +const char *LuaObject::s_type = "GunManager"; +template<> +void LuaObject::RegisterClass() +{ + lua_State *l = Lua::manager->GetLuaState(); + + LuaMetaType metaType(s_type); + + metaType.CreateMetaType(l); + + metaType.StartRecording() + .AddFunction("AddWeaponMount", &GunManager::AddWeaponMount) + .AddFunction("RemoveWeaponMount", &GunManager::RemoveWeaponMount) + .AddFunction("MountWeapon", &GunManager::MountWeapon) + .AddFunction("UnmountWeapon", &GunManager::UnmountWeapon) + .AddFunction("IsWeaponMounted", &GunManager::IsWeaponMounted) + .AddFunction("GetNumWeapons", &GunManager::GetNumWeapons) + .AddFunction("GetWeaponIndexForHardpoint", &GunManager::GetWeaponIndexForHardpoint) + .AddFunction("SetupDefaultGroups", &GunManager::SetupDefaultGroups) + .AddFunction("AssignWeaponToGroup", &GunManager::AssignWeaponToGroup) + .AddFunction("RemoveGroup", &GunManager::RemoveGroup) + .AddFunction("SetGroupTarget", &GunManager::SetGroupTarget) + .AddFunction("SetGroupFiring", &GunManager::SetGroupFiring) + .AddFunction("SetGroupFireWithoutTargeting", &GunManager::SetGroupFireWithoutTargeting) + .AddFunction("IsFiring", &GunManager::IsFiring) + .AddFunction("IsGroupFiring", &GunManager::IsGroupFiring); + metaType.StopRecording(); + + LuaObject::CreateClass(&metaType); +} From 876eb66f9d037cd848d285345772f206e984f31d Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 18:35:07 -0500 Subject: [PATCH 09/17] EquipType: use GunManager implementation - Weapon equipment now uses the GunManager API (with a shim to continue using the existing weapon data) - The GunManager has its weapon mount information populated from Lua on construction or ship type change --- data/libs/EquipType.lua | 43 ++++++++++++++++++++++++++++++++-------- data/libs/HullConfig.lua | 1 + data/libs/Ship.lua | 19 ++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/data/libs/EquipType.lua b/data/libs/EquipType.lua index b16238ce06f..982cf5fa3f5 100644 --- a/data/libs/EquipType.lua +++ b/data/libs/EquipType.lua @@ -272,17 +272,47 @@ end -- Base type for weapons ---@class Equipment.LaserType : EquipType ---@field laser_stats table +---@field weapon_data table local LaserType = utils.inherits(EquipType, "Equipment.LaserType") +function LaserType.New(specs) + local item = setmetatable(EquipType.New(specs), LaserType.meta) + local ls = specs.laser_stats + + -- NOTE: backwards-compatibility with old laser_stats definitions + if ls then + + local projectile = { + lifespan = ls.lifespan, + speed = ls.speed, + damage = ls.damage, + beam = ls.beam == 1, + mining = ls.mining == 1, + length = ls.length, + width = ls.width, + color = Color(ls.rgba_r, ls.rgba_g, ls.rgba_b, ls.rgba_a), + } + + item.weapon_data = { + rpm = 60 / ls.rechargeTime, + heatPerShot = ls.heatrate or 0.01, + cooling = ls.coolrate or 0.01, + overheat = 1.0, + projectile = projectile, + numBarrels = 1 + ls.dual + } + + end + + return item +end + ---@param ship Ship ---@param slot HullConfig.Slot function LaserType:OnInstall(ship, slot) EquipType.OnInstall(self, ship, slot) - for k, v in pairs(self.laser_stats) do - -- TODO: allow installing more than one laser - ship:setprop('laser_front_' .. k, v) - end + ship:GetComponent('GunManager'):MountWeapon(slot.id, self.weapon_data) end ---@param ship Ship @@ -290,10 +320,7 @@ end function LaserType:OnRemove(ship, slot) EquipType.OnRemove(self, ship, slot) - for k, v in pairs(self.laser_stats) do - -- TODO: allow installing more than one laser - ship:setprop('laser_front_' .. k, nil) - end + ship:GetComponent('GunManager'):UnmountWeapon(slot.id) end --============================================================================== diff --git a/data/libs/HullConfig.lua b/data/libs/HullConfig.lua index 42941aa12a7..43699fca364 100644 --- a/data/libs/HullConfig.lua +++ b/data/libs/HullConfig.lua @@ -27,6 +27,7 @@ Slot.hardpoint = false Slot.i18n_key = nil ---@type string? Slot.i18n_res = "equipment-core" Slot.count = nil ---@type integer? +Slot.gimbal = nil ---@type table? -- Class: HullConfig -- diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index 73984e1d58e..b1858ab7444 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -29,6 +29,8 @@ function Ship:Constructor() self:SetComponent('CargoManager', CargoManager.New(self)) self:SetComponent('EquipSet', EquipSet.New(self)) + self:UpdateWeaponSlots() + -- Timers cannot be started in ship constructors before Game is fully set, -- so trigger a lazy event to setup gameplay timers. -- @@ -40,6 +42,23 @@ end function Ship:OnShipTypeChanged() -- immediately update any needed components or properties self:GetComponent('EquipSet'):OnShipTypeChanged() + + self:UpdateWeaponSlots() +end + +---@private +function Ship:UpdateWeaponSlots() + local equipSet = self:GetComponent('EquipSet') + local gunManager = self:GetComponent('GunManager') + + for _, slot in ipairs(equipSet:GetAllSlotsOfType("weapon", true)) do + if not slot.gimbal then + print('Missing hardpoint gimbal on ship {} for slot {}' % { self.shipId, slot.id }) + end + + local gimbal = Vector2(table.unpack(slot.gimbal or { 1, 1 })) + gunManager:AddWeaponMount(slot.id, slot.tag, gimbal) + end end -- class method From efdab2a3e2f38c97062f27efd095fba2ba5b6647 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 18:47:26 -0500 Subject: [PATCH 10/17] Remove all usages of FixedGuns --- src/Beam.cpp | 1 + src/Body.h | 22 ++++++- src/DynamicBody.cpp | 1 - src/DynamicBody.h | 2 - src/Player.cpp | 7 -- src/Ship-AI.cpp | 4 +- src/Ship.cpp | 104 ++++++++++-------------------- src/Ship.h | 7 +- src/ShipAICmd.cpp | 34 ++++++---- src/ShipAICmd.h | 11 +++- src/WorldView.cpp | 15 +++-- src/lua/LuaShip.cpp | 3 +- src/ship/PlayerShipController.cpp | 3 +- 13 files changed, 107 insertions(+), 107 deletions(-) diff --git a/src/Beam.cpp b/src/Beam.cpp index 3ab956376b8..ce3cda634d4 100644 --- a/src/Beam.cpp +++ b/src/Beam.cpp @@ -9,6 +9,7 @@ #include "GameSaveError.h" #include "JsonUtils.h" #include "Pi.h" +#include "Projectile.h" #include "Planet.h" #include "Player.h" #include "Sfx.h" diff --git a/src/Body.h b/src/Body.h index 19b01cdbb36..c4b46cc466a 100644 --- a/src/Body.h +++ b/src/Body.h @@ -152,20 +152,34 @@ class Body : public DeleteEmitter, public PropertiedObject { T *GetComponent() const { auto *type = BodyComponentDB::GetComponentType(); - return m_components & (uint64_t(1) << uint8_t(type->componentIndex)) ? type->get(this) : nullptr; + return m_components & GetComponentBit(type->componentIndex) ? type->get(this) : nullptr; } + // Add a component to this body if it is not already present. + // Returns a pointer to the existing component if a new one could not be added. template T *AddComponent() { auto *type = BodyComponentDB::GetComponentType(); - if (m_components & (uint64_t(1) << uint8_t(type->componentIndex))) + if (m_components & GetComponentBit(type->componentIndex)) return type->get(this); - m_components |= (uint64_t(1) << uint8_t(type->componentIndex)); + m_components |= GetComponentBit(type->componentIndex); return type->newComponent(this); } + // Remove a component from this body, destroying it. + template + void RemoveComponent() + { + auto *type = BodyComponentDB::GetComponentType(); + if (!(m_components & GetComponentBit(type->componentIndex))) + return; + + m_components ^= GetComponentBit(type->componentIndex); + type->deleteComponent(this); + } + // Returns the bitset of components attached to this body. Prefer using HasComponent<> or GetComponent<> instead. uint64_t GetComponentList() const { return m_components; } @@ -218,6 +232,8 @@ class Body : public DeleteEmitter, public PropertiedObject { matrix3x3d m_interpOrient; private: + uint64_t GetComponentBit(uint8_t bit) const { return uint64_t(1) << bit; } + vector3d m_pos; matrix3x3d m_orient; FrameId m_frame; // frame of reference diff --git a/src/DynamicBody.cpp b/src/DynamicBody.cpp index f506e458aa2..766f0ea1123 100644 --- a/src/DynamicBody.cpp +++ b/src/DynamicBody.cpp @@ -3,7 +3,6 @@ #include "DynamicBody.h" -#include "FixedGuns.h" #include "Frame.h" #include "GameSaveError.h" #include "JsonUtils.h" diff --git a/src/DynamicBody.h b/src/DynamicBody.h index 410b9f8bf01..af37835c04a 100644 --- a/src/DynamicBody.h +++ b/src/DynamicBody.h @@ -9,13 +9,11 @@ #include "vector3.h" class Propulsion; -class FixedGuns; class Orbit; class DynamicBody : public ModelBody { private: friend class Propulsion; - friend class FixedGuns; public: OBJDEF(DynamicBody, ModelBody, DYNAMICBODY); diff --git a/src/Player.cpp b/src/Player.cpp index f3754e4b2a6..afea2f7e212 100644 --- a/src/Player.cpp +++ b/src/Player.cpp @@ -3,7 +3,6 @@ #include "Player.h" -#include "FixedGuns.h" #include "Frame.h" #include "Game.h" #include "GameConfig.h" @@ -32,7 +31,6 @@ Player::Player(const ShipType::Id &shipId) : { SetController(new PlayerShipController()); InitCockpit(); - m_fixedGuns->SetShouldUseLeadCalc(true); m_atmosAccel = vector3d(0.0f, 0.0f, 0.0f); } @@ -40,7 +38,6 @@ Player::Player(const Json &jsonObj, Space *space) : Ship(jsonObj, space) { InitCockpit(); - m_fixedGuns->SetShouldUseLeadCalc(true); } void Player::SetShipType(const ShipType::Id &shipId) @@ -280,10 +277,6 @@ void Player::StaticUpdate(const float timeStep) { Ship::StaticUpdate(timeStep); - for (size_t i = 0; i < GUNMOUNT_MAX; i++) - if (m_fixedGuns->IsGunMounted(i)) - m_fixedGuns->UpdateLead(timeStep, i, this, GetCombatTarget()); - // now insert the latest value vector3d current_atmosAccel = GetAtmosForce() * (1.0 / GetMass()); m_atmosJerk = (current_atmosAccel - m_atmosAccel) * Pi::game->GetInvTimeAccelRate(); diff --git a/src/Ship-AI.cpp b/src/Ship-AI.cpp index cdc8838a07b..43474f45791 100644 --- a/src/Ship-AI.cpp +++ b/src/Ship-AI.cpp @@ -8,6 +8,7 @@ #include "Space.h" #include "SpaceStation.h" #include "lua/LuaEvent.h" +#include "ship/GunManager.h" #include "profiler/Profiler.h" @@ -24,8 +25,7 @@ bool Ship::AITimeStep(float timeStep) // just in case the AI left it on ClearThrusterState(); - for (int i = 0; i < Guns::GUNMOUNT_MAX; i++) - SetGunState(i, 0); + m_gunManager->SetAllGroupsFiring(false); return true; } diff --git a/src/Ship.cpp b/src/Ship.cpp index 316847f3e18..87337540093 100644 --- a/src/Ship.cpp +++ b/src/Ship.cpp @@ -30,6 +30,7 @@ #include "scenegraph/Animation.h" #include "scenegraph/Tag.h" #include "scenegraph/CollisionGeometry.h" +#include "ship/GunManager.h" #include "ship/PlayerShipController.h" static const float TONS_HULL_PER_SHIELD = 10.f; @@ -52,8 +53,8 @@ Ship::Ship(const ShipType::Id &shipId) : THIS CODE DOES NOT RUN WHEN LOADING SAVEGAMES!! */ m_propulsion = AddComponent(); - m_fixedGuns = AddComponent(); m_shields = AddComponent(); + m_gunManager = AddComponent(); Properties().Set("flightState", EnumStrings::GetString("ShipFlightState", m_flightState)); Properties().Set("alertStatus", EnumStrings::GetString("ShipAlertStatus", m_alertState)); @@ -78,7 +79,6 @@ Ship::Ship(const ShipType::Id &shipId) : m_hyperspace.countdown = 0; m_hyperspace.now = false; - m_fixedGuns->Init(this); m_ecmRecharge = 0; m_shieldCooldown = 0.0f; m_curAICmd = 0; @@ -118,8 +118,8 @@ Ship::Ship(const Json &jsonObj, Space *space) : DynamicBody(jsonObj, space) { m_propulsion = AddComponent(); - m_fixedGuns = AddComponent(); m_shields = AddComponent(); + m_gunManager = AddComponent(); try { Json shipObj = jsonObj["ship"]; @@ -156,8 +156,6 @@ Ship::Ship(const Json &jsonObj, Space *space) : m_hyperspace.sounds.abort_sound = shipObj.value("hyperspace_abort_sound", ""); m_hyperspace.sounds.jump_sound = shipObj.value("hyperspace_jump_sound", ""); - m_fixedGuns->LoadFromJson(shipObj, space); - m_ecmRecharge = shipObj["ecm_recharge"]; SetShipId(shipObj["ship_type_id"]); // XXX handle missing thirdparty ship m_dockedWithPort = shipObj["docked_with_port"]; @@ -227,6 +225,9 @@ void Ship::Init() p.Set("shieldMassLeft", m_stats.shield_mass_left); p.Set("fuelMassLeft", m_stats.fuel_tank_mass_left); + // Init of GunManager + m_gunManager->Init(this); + // Init of Propulsion: m_propulsion->Init(this, GetModel(), m_type->fuelTankMass, m_type->effectiveExhaustVelocity, m_type->linThrust, m_type->angThrust, m_type->linAccelerationCap); @@ -238,8 +239,6 @@ void Ship::Init() m_landingGearAnimation = GetModel()->FindAnimation("gear_down"); m_forceWheelUpdate = true; - m_fixedGuns->InitGuns(GetModel()); - // If we've got the tag_landing set then use it for an offset // otherwise use zero so that it will dock but look clearly incorrect const SceneGraph::Tag *tagNode = GetModel()->FindTagByName("tag_landing"); @@ -286,8 +285,6 @@ void Ship::SaveToJson(Json &jsonObj, Space *space) shipObj["hyperspace_abort_sound"] = m_hyperspace.sounds.abort_sound; shipObj["hyperspace_jump_sound"] = m_hyperspace.sounds.jump_sound; - m_fixedGuns->SaveToJson(shipObj, space); - shipObj["ecm_recharge"] = m_ecmRecharge; shipObj["ship_type_id"] = m_type->id; shipObj["docked_with_port"] = m_dockedWithPort; @@ -626,7 +623,6 @@ void Ship::UpdateEquipStats() p.Set("shieldMass", m_stats.shield_mass); UpdateFuelStats(); - UpdateGunsStats(); unsigned int thruster_power_cap = p.Get("thruster_power_cap"); const double power_mul = m_type->thrusterUpgrades[Clamp(thruster_power_cap, 0U, 3U)]; @@ -664,43 +660,6 @@ void Ship::UpdateLuaStats() p.Set("maxHyperspaceRange", m_stats.hyperspace_range_max); } -void Ship::UpdateGunsStats() -{ - PropertyMap &prop = Properties(); - float cooler = prop.Get("laser_cooler_cap"); - m_fixedGuns->SetCoolingBoost(cooler ? cooler : 1.0f); - - for (int num = 0; num < 2; num++) { - std::string prefix(num ? "laser_rear_" : "laser_front_"); - int damage = prop.Get(prefix + "damage"); - if (!damage) { - m_fixedGuns->UnMountGun(num); - } else { - const Color c( - prop.Get(prefix + "rgba_r"), - prop.Get(prefix + "rgba_g"), - prop.Get(prefix + "rgba_b"), - prop.Get(prefix + "rgba_a")); - const float heatrate = prop.Get(prefix + "heatrate").get_number(0.01f); - const float coolrate = prop.Get(prefix + "coolrate").get_number(0.01f); - const float lifespan = prop.Get(prefix + "lifespan"); - const float width = prop.Get(prefix + "width"); - const float length = prop.Get(prefix + "length"); - const float speed = prop.Get(prefix + "speed"); - const float recharge = prop.Get(prefix + "rechargeTime"); - const bool mining = prop.Get(prefix + "mining").get_integer(); - const bool beam = prop.Get(prefix + "beam").get_integer(); - - m_fixedGuns->MountGun(num, recharge, lifespan, damage, length, width, mining, c, speed, beam, heatrate, coolrate); - - if (prop.Get(prefix + "dual").get_integer()) - m_fixedGuns->IsDual(num, true); - else - m_fixedGuns->IsDual(num, false); - } - } -} - void Ship::UpdateFuelStats() { m_stats.fuel_tank_mass_left = m_propulsion->FuelTankMassLeft(); @@ -1103,7 +1062,7 @@ void Ship::UpdateAlertState() if (GetPositionRelTo(ship).LengthSqr() < ALERT_DISTANCE * ALERT_DISTANCE) { ship_is_near = true; - Uint32 gunstate = ship->m_fixedGuns->IsFiring(); + Uint32 gunstate = ship->m_gunManager->IsFiring(); if (gunstate) { ship_is_firing = true; break; @@ -1274,36 +1233,38 @@ void Ship::StaticUpdate(const float timeStep) m_launchLockTimeout = 0; // lasers - FixedGuns *fg = m_fixedGuns; - fg->UpdateGuns(timeStep); - for (int i = 0; i < 2; i++) { - if (fg->Fire(i, this)) { - if (fg->IsBeam(i)) { - float vl, vr; - Sound::CalculateStereo(this, 1.0f, &vl, &vr); - m_beamLaser[i].Play("Beam_laser", vl, vr, Sound::OP_REPEAT); - } else { - Sound::BodyMakeNoise(this, "Pulse_Laser", 1.0f); - } - LuaEvent::Queue("onShipFiring", this); - } + GetComponent()->StaticUpdate(timeStep); - if (fg->IsBeam(i)) { - if (fg->IsFiring(i)) { + // TODO: this is abominable. + // It will lead to multiple sound cutouts with more than a single beam laser + // Unfortunately, I don't have the time or inclination to completely rewrite the sound system at this juncture + + GunManager::WeaponIndexSet firedGuns = m_gunManager->GetGunsFiredThisFrame(); + GunManager::WeaponIndexSet stoppedGuns = m_gunManager->GetGunsStoppedThisFrame(); + + for (size_t i = 0; i < m_gunManager->GetNumWeapons(); i++) { + const GunManager::WeaponState *ws = m_gunManager->GetWeaponState(i); + if (ws->data.projectileType == GunManager::PROJECTILE_BEAM) { + if (m_gunManager->GetWeaponGroups()[ws->group].firing) { float vl, vr; Sound::CalculateStereo(this, 1.0f, &vl, &vr); - if (!m_beamLaser[i].IsPlaying()) { - m_beamLaser[i].Play("Beam_laser", vl, vr, Sound::OP_REPEAT); + if (firedGuns[i]) { + m_beamLaser[i % 2].Play("Beam_laser", vl, vr, Sound::OP_REPEAT); } else { // update volume - m_beamLaser[i].SetVolume(vl, vr); + m_beamLaser[i % 2].SetVolume(vl, vr); } - } else if (!fg->IsFiring(i) && m_beamLaser[i].IsPlaying()) { - m_beamLaser[i].Stop(); + } else if (stoppedGuns[i]) { + m_beamLaser[i % 2].Stop(); } + } else if (firedGuns[i]) { + Sound::BodyMakeNoise(this, "Pulse_Laser", 1.0f); } } + if (firedGuns.any()) + LuaEvent::Queue("onShipFiring", this); + if (m_ecmRecharge > 0.0f) { m_ecmRecharge = std::max(0.0f, m_ecmRecharge - timeStep); } @@ -1414,7 +1375,7 @@ void Ship::SetGunState(int idx, int state) if (m_flightState != FLYING) return; - m_fixedGuns->SetGunFiringState(idx, state); + m_gunManager->SetGroupFiring(idx, state); } bool Ship::SetWheelState(bool down) @@ -1572,6 +1533,11 @@ void Ship::SetShipType(const ShipType::Id &shipId) SetModel(m_type->modelName.c_str()); SetupShields(); + // Recreate our GunManager for the new ship type + // Its init method will be called later + RemoveComponent(); + m_gunManager = AddComponent(); + m_skin.SetDecal(m_type->manufacturer); m_skin.Apply(GetModel()); Init(); diff --git a/src/Ship.h b/src/Ship.h index 416057fe062..4ff438ca9c1 100644 --- a/src/Ship.h +++ b/src/Ship.h @@ -13,7 +13,6 @@ #include "scenegraph/ModelSkin.h" #include "sound/Sound.h" -#include "FixedGuns.h" #include "ship/Propulsion.h" class AICommand; @@ -27,6 +26,7 @@ class Planet; class Sensors; class ShipController; class Space; +class GunManager; struct CollisionContact; struct HeatGradientParameters_t; @@ -96,12 +96,11 @@ class Ship : public DynamicBody { void UpdateLuaStats(); void UpdateEquipStats(); void UpdateFuelStats(); - void UpdateGunsStats(); const shipstats_t &GetStats() const { return m_stats; } void Explode(); virtual bool DoDamage(float kgDamage); // can be overloaded in Player to add audio - void SetGunState(int idx, int state); + [[deprecated]] void SetGunState(int idx, int state); void UpdateMass(); virtual bool SetWheelState(bool down); // returns success of state change, NOT state itself virtual bool ManualDocking() const { return false; } @@ -273,7 +272,7 @@ class Ship : public DynamicBody { } m_hyperspace; Propulsion *m_propulsion; - FixedGuns *m_fixedGuns; + GunManager *m_gunManager; Shields *m_shields; private: diff --git a/src/ShipAICmd.cpp b/src/ShipAICmd.cpp index 1750a9d2419..d332d4c52b9 100644 --- a/src/ShipAICmd.cpp +++ b/src/ShipAICmd.cpp @@ -12,6 +12,7 @@ #include "Space.h" #include "SpaceStation.h" #include "perlin.h" +#include "ship/GunManager.h" #include "ship/Propulsion.h" static const double VICINITY_MIN = 15000.0; @@ -327,9 +328,9 @@ AICmdKill::AICmdKill(DynamicBody *dBody, Ship *target) : m_leadTime = m_evadeTime = m_closeTime = 0.0; m_lastVel = m_target->GetVelocity(); m_prop = m_dBody->GetComponent(); - m_fguns = m_dBody->GetComponent(); + m_guns = m_dBody->GetComponent(); assert(m_prop != nullptr); - assert(m_fguns != nullptr); + assert(m_guns != nullptr); } AICmdKill::AICmdKill(const Json &jsonObj) : @@ -349,7 +350,7 @@ void AICmdKill::SaveToJson(Json &jsonObj) AICmdKill::~AICmdKill() { - if (m_fguns) m_fguns->SetGunFiringState(0, 0); + m_guns->SetAllGroupsFiring(false); } void AICmdKill::PostLoadFixup(Space *space) @@ -360,9 +361,19 @@ void AICmdKill::PostLoadFixup(Space *space) m_lastVel = m_target->GetVelocity(); // Ensure needed sub-system: m_prop = m_dBody->GetComponent(); - m_fguns = m_dBody->GetComponent(); + m_guns = m_dBody->GetComponent(); assert(m_prop != nullptr); - assert(m_fguns != nullptr); + assert(m_guns != nullptr); +} + +AICmdKill::GunStats AICmdKill::GetGunStats() +{ + if (!m_guns->GetNumWeapons()) + return { 0.f, 0.f }; + + const GunManager::WeaponData *data = &m_guns->GetWeapons().front().data; + + return { data->projectile.speed * data->projectile.lifespan, data->projectile.speed }; } bool AICmdKill::TimeStepUpdate() @@ -396,6 +407,8 @@ bool AICmdKill::TimeStepUpdate() return false; } + GunStats stats = GetGunStats(); + dist = sqrt(dist); const matrix3x3d &rot = m_dBody->GetOrient(); vector3d targvel = m_target->GetVelocityRelTo(m_dBody); @@ -404,7 +417,7 @@ bool AICmdKill::TimeStepUpdate() // Accel will be wrong for a frame on timestep changes, but it doesn't matter vector3d targaccel = (m_target->GetVelocity() - m_lastVel) / Pi::game->GetTimeStep(); m_lastVel = m_target->GetVelocity(); // may need next frame - vector3d leaddir = m_prop->AIGetLeadDir(m_target, targaccel, m_fguns->GetProjSpeed(0)); + vector3d leaddir = m_prop->AIGetLeadDir(m_target, targaccel, stats.speed); if (dist >= VICINITY_MIN + 1000.0) { // if really far from target, intercept // Output("%s started AUTOPILOT\n", m_ship->GetLabel().c_str()); @@ -434,12 +447,11 @@ bool AICmdKill::TimeStepUpdate() double vissize = 1.3 * m_dBody->GetPhysRadius() / dist; vissize += (0.05 + 0.5 * leaddiff) * Pi::rng.Double() * skillShoot; if (vissize > headdiff) - m_fguns->SetGunFiringState(0, 1); + m_guns->SetAllGroupsFiring(true); else - m_fguns->SetGunFiringState(0, 0); - float max_fire_dist = m_fguns->GetGunRange(0); - if (max_fire_dist > 4000) max_fire_dist = 4000; - if (dist > max_fire_dist) m_fguns->SetGunFiringState(0, 0); // temp + m_guns->SetAllGroupsFiring(false); + float max_fire_dist = std::min(stats.range, 4000.f); + if (dist > max_fire_dist) m_guns->SetAllGroupsFiring(false); // temp } m_leadOffset += m_leadDrift * Pi::game->GetTimeStep(); double leadAV = (leaddir - targdir).Dot((leaddir - heading).NormalizedSafe()); // leaddir angvel diff --git a/src/ShipAICmd.h b/src/ShipAICmd.h index 439ceaddb24..938432f1832 100644 --- a/src/ShipAICmd.h +++ b/src/ShipAICmd.h @@ -8,10 +8,10 @@ #include "JsonFwd.h" #include "DynamicBody.h" -#include "FixedGuns.h" #include "FrameId.h" #include "ship/Propulsion.h" +class GunManager; class Ship; class Space; class SpaceStation; @@ -193,7 +193,14 @@ class AICmdKill : public AICommand { const Ship* GetTarget() const { return m_target; } private: - FixedGuns *m_fguns; + struct GunStats { + float range; + float speed; + }; + + GunStats GetGunStats(); + + GunManager *m_guns; Ship *m_target; double m_leadTime, m_evadeTime, m_closeTime; vector3d m_leadOffset, m_leadDrift, m_lastVel; diff --git a/src/WorldView.cpp b/src/WorldView.cpp index e27c9a4a300..dfe53469db7 100644 --- a/src/WorldView.cpp +++ b/src/WorldView.cpp @@ -22,6 +22,7 @@ #include "graphics/Renderer.h" #include "graphics/RenderState.h" #include "matrix4x4.h" +#include "ship/GunManager.h" #include "ship/ShipViewController.h" #include "sound/Sound.h" @@ -332,11 +333,17 @@ void WorldView::UpdateProjectedObjects() } } - FixedGuns *gunManager = Pi::player->GetComponent(); - if (laser >= 0 && gunManager->IsGunMounted(laser) && gunManager->IsFiringSolutionOk()) { - UpdateIndicator(m_targetLeadIndicator, cam_rot * gunManager->GetTargetLeadPos()); - if ((m_targetLeadIndicator.side != INDICATOR_ONSCREEN) || (m_combatTargetIndicator.side != INDICATOR_ONSCREEN)) + GunManager *gunManager = Pi::player->GetComponent(); + if (laser >= 0 && laser < gunManager->GetWeaponGroups().size()) { + const auto &group = gunManager->GetWeaponGroups()[laser]; + + if (gunManager->GetGroupTarget(laser)) { + UpdateIndicator(m_targetLeadIndicator, cam_rot * gunManager->GetGroupLeadPos(laser)); + if ((m_targetLeadIndicator.side != INDICATOR_ONSCREEN) || (m_combatTargetIndicator.side != INDICATOR_ONSCREEN)) + HideIndicator(m_targetLeadIndicator); + } else { HideIndicator(m_targetLeadIndicator); + } } else { HideIndicator(m_targetLeadIndicator); } diff --git a/src/lua/LuaShip.cpp b/src/lua/LuaShip.cpp index 7f73d7b1b8c..c874547cdad 100644 --- a/src/lua/LuaShip.cpp +++ b/src/lua/LuaShip.cpp @@ -17,6 +17,7 @@ #include "ShipType.h" #include "Space.h" #include "SpaceStation.h" +#include "ship/GunManager.h" #include "ship/PlayerShipController.h" #include "ship/PrecalcPath.h" #include "lua.h" @@ -1147,7 +1148,7 @@ static int l_ship_get_gun_temperature(lua_State *l) { Ship *s = LuaObject::CheckFromLua(1); int gun = luaL_checkinteger(l, 2); - LuaPush(l, s->GetComponent()->GetGunTemperature(gun)); + LuaPush(l, s->GetComponent()->GetGroupTemperatureState(gun)); return 1; } diff --git a/src/ship/PlayerShipController.cpp b/src/ship/PlayerShipController.cpp index d35fa3abc50..c5e8d25d758 100644 --- a/src/ship/PlayerShipController.cpp +++ b/src/ship/PlayerShipController.cpp @@ -15,8 +15,8 @@ #include "Space.h" #include "SystemView.h" #include "WorldView.h" -#include "core/OS.h" #include "lua/LuaObject.h" +#include "ship/GunManager.h" #include "ship/ShipController.h" #include "Sensors.h" @@ -968,6 +968,7 @@ void PlayerShipController::SetCombatTarget(Body *const target, bool setFollowTo) SetFollowTarget(target); m_combatTarget = target; + m_ship->GetComponent()->SetTrackingTarget(target); onChangeTarget.emit(); } From 597039e0ee29bf45b37250b5c6f1e7baf5acc104 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Wed, 4 Dec 2024 20:56:03 -0500 Subject: [PATCH 11/17] Remove FixedGuns --- src/FixedGuns.cpp | 321 ---------------------------------------------- src/FixedGuns.h | 82 ------------ 2 files changed, 403 deletions(-) delete mode 100644 src/FixedGuns.cpp delete mode 100644 src/FixedGuns.h diff --git a/src/FixedGuns.cpp b/src/FixedGuns.cpp deleted file mode 100644 index 9d9df0b382a..00000000000 --- a/src/FixedGuns.cpp +++ /dev/null @@ -1,321 +0,0 @@ -// Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt - -#include "FixedGuns.h" -#include "Beam.h" -#include "DynamicBody.h" -#include "GameSaveError.h" -#include "JsonUtils.h" -#include "MathUtil.h" -#include "Projectile.h" -#include "Quaternion.h" -#include "StringF.h" -#include "scenegraph/Tag.h" -#include "vector3.h" - -REGISTER_COMPONENT_TYPE(FixedGuns) { - BodyComponentDB::RegisterComponent("FixedGuns"); -} - -FixedGuns::FixedGuns() -{ -} - -FixedGuns::~FixedGuns() -{ -} - -bool FixedGuns::IsFiring() -{ - bool gunstate = false; - for (int j = 0; j < Guns::GUNMOUNT_MAX; j++) - gunstate |= m_is_firing[j]; - return gunstate; -} - -bool FixedGuns::IsFiring(const int num) -{ - return m_is_firing[num]; -} - -bool FixedGuns::IsBeam(const int num) -{ - return m_gun[num].projData.beam; -} - -void FixedGuns::Init(DynamicBody *b) -{ - for (int i = 0; i < Guns::GUNMOUNT_MAX; i++) { - // Initialize structs - m_is_firing[i] = false; - m_gun[i].recharge = 0; - m_gun[i].temp_heat_rate = 0; - m_gun[i].temp_cool_rate = 0; - m_gun[i].projData.lifespan = 0; - m_gun[i].projData.damage = 0; - m_gun[i].projData.length = 0; - m_gun[i].projData.width = 0; - m_gun[i].projData.mining = false; - m_gun[i].projData.speed = 0; - m_gun[i].projData.color = Color::BLACK; - // Set there's no guns: - m_gun_present[i] = false; - m_recharge_stat[i] = 0.0; - m_temperature_stat[i] = 0.0; - } -} - -void FixedGuns::SaveToJson(Json &jsonObj, Space *space) -{ - - Json gunArray = Json::array(); // Create JSON array to contain gun data. - - for (int i = 0; i < Guns::GUNMOUNT_MAX; i++) { - Json gunArrayEl = Json::object(); // Create JSON object to contain gun. - gunArrayEl["state"] = m_is_firing[i]; - gunArrayEl["recharge"] = m_recharge_stat[i]; - gunArrayEl["temperature"] = m_temperature_stat[i]; - gunArray.push_back(gunArrayEl); // Append gun object to array. - } - jsonObj["guns"] = gunArray; // Add gun array to ship object. -} - -void FixedGuns::LoadFromJson(const Json &jsonObj, Space *space) -{ - Json gunArray = jsonObj["guns"].get(); - assert(Guns::GUNMOUNT_MAX == gunArray.size()); - - try { - for (unsigned int i = 0; i < Guns::GUNMOUNT_MAX; i++) { - Json gunArrayEl = gunArray[i]; - - m_is_firing[i] = gunArrayEl["state"]; - m_recharge_stat[i] = gunArrayEl["recharge"]; - m_temperature_stat[i] = gunArrayEl["temperature"]; - } - } catch (Json::type_error &) { - throw SavedGameCorruptException(); - } -} - -void FixedGuns::InitGuns(SceneGraph::Model *m) -{ - for (int num = 0; num < Guns::GUNMOUNT_MAX; num++) { - int found = 0; - // probably 4 is fine 99% of the time (X-Wings) - m_gun[num].locs.reserve(4); - // 32 is a crazy number... - for (int gun = 0; gun < 32; gun++) { - const std::string tag = stringf("tag_gunmount_%0{d}_multi_%1{d}", num, gun); //"gunmount_0_multi_0"; - const SceneGraph::Tag *tagNode = m->FindTagByName(tag); - if (tagNode) { - ++found; - const matrix4x4f &trans = tagNode->GetGlobalTransform(); - GunData::GunLoc loc; - loc.pos = vector3d(trans.GetTranslate()); - loc.dir = vector3d(trans.GetOrient().VectorZ()); - m_gun[num].locs.push_back(loc); - } else if (found == 0) { - // look for legacy "tag_gunmount_0" or "tag_gunmount_1" tags - const std::string tag = stringf("tag_gunmount_%0{d}", num); //"gunmount_0"; - const SceneGraph::Tag *tagNode = m->FindTagByName(tag); - if (tagNode) { - ++found; - const matrix4x4f &trans = tagNode->GetGlobalTransform(); - GunData::GunLoc loc; - loc.pos = vector3d(trans.GetTranslate()); - loc.dir = vector3d(trans.GetOrient().VectorZ()); - m_gun[num].locs.push_back(loc); - } - break; // definitely no more "gun"s for this "num" if we've come down this path - } else - break; - } - } -} - -void FixedGuns::MountGun(const int num, const float recharge, const float lifespan, const float damage, const float length, - const float width, const bool mining, const Color &color, const float speed, const bool beam, const float heatrate, const float coolrate) -{ - if (num >= Guns::GUNMOUNT_MAX) - return; - // Here we have projectile data MORE recharge time - m_is_firing[num] = false; - m_gun[num].recharge = recharge; - m_gun[num].temp_heat_rate = heatrate; // TODO: More fun if you have a variable "speed" for temperature - m_gun[num].temp_cool_rate = coolrate; - m_gun[num].projData.lifespan = lifespan; - m_gun[num].projData.damage = damage; - m_gun[num].projData.length = length; - m_gun[num].projData.width = width; - m_gun[num].projData.mining = mining; - m_gun[num].projData.color = color; - m_gun[num].projData.speed = speed; - m_gun[num].projData.beam = beam; - m_gun_present[num] = true; -} - -void FixedGuns::UnMountGun(int num) -{ - if (num >= Guns::GUNMOUNT_MAX) - return; - if (!m_gun_present[num]) - return; - m_is_firing[num] = false; - m_gun[num].recharge = 0; - m_gun[num].temp_heat_rate = 0; - m_gun[num].temp_cool_rate = 0; - m_gun[num].projData.lifespan = 0; - m_gun[num].projData.damage = 0; - m_gun[num].projData.length = 0; - m_gun[num].projData.width = 0; - m_gun[num].projData.mining = false; - m_gun[num].projData.speed = 0; - m_gun[num].projData.color = Color::BLACK; - m_gun_present[num] = false; -} - -void FixedGuns::SetGunFiringState(int idx, int s) -{ - if (m_gun_present[idx]) - m_is_firing[idx] = s; -} - -bool FixedGuns::Fire(int num, Body *b) -{ - if (!m_gun_present[num]) return false; - if (!m_is_firing[num]) return false; - // Output("Firing gun %i, present\n", num); - // Output(" is firing\n"); - if (m_recharge_stat[num] > 0) return false; - // Output(" recharge stat <= 0\n"); - if (m_temperature_stat[num] > 1.0) return false; - // Output(" temperature stat <= 1.0\n"); - - m_temperature_stat[num] += m_gun[num].temp_heat_rate; - m_recharge_stat[num] = m_gun[num].recharge; - - const int maxBarrels = std::min(size_t(m_gun[num].dual ? 2 : 1), m_gun[num].locs.size()); - - for (int iBarrel = 0; iBarrel < maxBarrels; iBarrel++) { - //m_currentLeadDir already points in direction to shoot - corrected with aim assist if near the crosshair - const vector3d dir = b->GetOrient() * (m_shouldUseLeadCalc ? m_currentLeadDir : vector3d(m_gun[num].locs[iBarrel].dir)); - const vector3d pos = b->GetOrient() * vector3d(m_gun[num].locs[iBarrel].pos) + b->GetPosition(); - - if (m_gun[num].projData.beam) { - Beam::Add(b, m_gun[num].projData, pos, b->GetVelocity(), dir); - } else { - const vector3d dirVel = m_gun[num].projData.speed * dir; - Projectile::Add(b, m_gun[num].projData, pos, b->GetVelocity(), dirVel); - } - } - - return true; -} - -void FixedGuns::UpdateGuns(float timeStep) -{ - for (int i = 0; i < Guns::GUNMOUNT_MAX; i++) { - if (!m_gun_present[i]) - continue; - - m_recharge_stat[i] -= timeStep; - - float rateCooling = m_gun[i].temp_cool_rate; - rateCooling *= m_cooler_boost; - m_temperature_stat[i] -= rateCooling * timeStep; - - if (m_temperature_stat[i] < 0.0f) - m_temperature_stat[i] = 0; - else if (m_temperature_stat[i] > 1.0f) - m_is_firing[i] = false; - - if (m_recharge_stat[i] < 0.0f) - m_recharge_stat[i] = 0; - } -} - -static constexpr double MAX_LEAD_ANGLE = DEG2RAD(1.5); - -void FixedGuns::UpdateLead(float timeStep, int num, Body *ship, Body *target) -{ - assert(num < GUNMOUNT_MAX); - const vector3d forwardVector = num == GUN_REAR ? vector3d(0, 0, 1) : vector3d(0, 0, -1); - m_targetLeadPos = forwardVector; - m_firingSolutionOk = false; - - if (!target) { - m_currentLeadDir = forwardVector; - return; - } - - const vector3d targpos = target->GetPositionRelTo(ship) * ship->GetOrient(); - - // calculate firing solution and relative velocity along our z axis - double projspeed = m_gun[num].projData.speed; - - // don't calculate lead if there's no gun there - if (m_gun_present[num] && projspeed > 0) { - if (m_gun[num].projData.beam) { - //For beems just shoot where the target is - no lead needed - m_targetLeadPos = targpos; - } else { - const vector3d targvel = target->GetVelocityRelTo(ship) * ship->GetOrient(); - - //Exact lead calculation. We start with: - // |targpos * l + targvel| = projspeed - //we solve for l which can be interpreted as 1/time for the projectile to reach the target - //it gives: - // |targpos|^2 * l^2 + targpos*targvel * 2l + |targvel|^2 - projspeed^2 = 0; - // so it gives scalar quadratic equation with two possible solutions - we care only about the positive one - shooting forward - // A basic math for solving, there is probably more elegant and efficient way to do this: - double a = targpos.LengthSqr(); - double b = targpos.Dot(targvel) * 2; - double c = targvel.LengthSqr() - projspeed * projspeed; - double delta = b * b - 4 * a * c; - - if (delta < 0) { - //no solution - m_currentLeadDir = forwardVector; - return; - } - - //l = (-b + sqrt(delta)) / 2a; t=1/l; a>0 - double t = 2 * a / (-b + sqrt(delta)); - - if (t < 0 || t > m_gun[num].projData.lifespan) { - //no positive solution or target too far - m_currentLeadDir = forwardVector; - return; - } else { - //This is an exact solution as opposed to 2 step approximation used before. - //It does not improve the accuracy as expected though. - //If the target is accelerating and is far enough then this aim assist will - //actually make sure that it is mpossible to hit.. - m_targetLeadPos = targpos + targvel * t; - - //lets try to adjust for acceleration of the target ship - if (target->IsType(ObjectType::SHIP)) { - DynamicBody *targetShip = static_cast(target); - vector3d acc = targetShip->GetLastForce() * ship->GetOrient() / targetShip->GetMass(); - //s=a*t^2/2 -> hitting steadily accelerating ships works at much greater distance - m_targetLeadPos += acc * t * t * 0.5; - } - } - } - } - - const vector3d targetDir = m_targetLeadPos.Normalized(); - const vector3d gunLeadTarget = (targetDir.Dot(forwardVector) >= cos(MAX_LEAD_ANGLE)) ? targetDir : forwardVector; - - m_currentLeadDir = gunLeadTarget; - m_firingSolutionOk = true; -} - -float FixedGuns::GetGunTemperature(int idx) const -{ - if (m_gun_present[idx]) - return m_temperature_stat[idx]; - else - return 0.0f; -} diff --git a/src/FixedGuns.h b/src/FixedGuns.h deleted file mode 100644 index 572feb13c97..00000000000 --- a/src/FixedGuns.h +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details -// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt - -#ifndef FIXEDGUNS_H -#define FIXEDGUNS_H - -#include "JsonFwd.h" -#include "Projectile.h" -#include "scenegraph/Model.h" -#include "vector3.h" - -class DynamicBody; -class Space; - -enum Guns { - GUN_FRONT, - GUN_REAR, - GUNMOUNT_MAX = 2 -}; - -class FixedGuns : public RefCounted { -public: - FixedGuns(); - virtual ~FixedGuns(); - void Init(DynamicBody *b); - void InitGuns(SceneGraph::Model *m); - - virtual void SaveToJson(Json &jsonObj, Space *space); - virtual void LoadFromJson(const Json &jsonObj, Space *space); - - void UpdateGuns(float timeStep); - void UpdateLead(float timeStep, int num, Body *ship, Body *target); - bool Fire(int num, Body *ship); - void SetGunFiringState(int idx, int s); - void SetShouldUseLeadCalc(bool enable) { m_shouldUseLeadCalc = enable; } - - bool IsFiring(); - bool IsFiring(const int num); - bool IsBeam(const int num); - inline void IsDual(int idx, bool dual) { m_gun[idx].dual = dual; }; - - void MountGun(const int num, const float recharge, const float lifespan, const float damage, const float length, - const float width, const bool mining, const Color &color, const float speed, const bool beam, const float heatrate, const float coolrate); - void UnMountGun(int num); - - float GetGunTemperature(int idx) const; - inline bool IsGunMounted(int idx) const { return m_gun_present[idx]; } - inline float GetGunRange(int idx) const { return m_gun[idx].projData.speed * m_gun[idx].projData.lifespan; }; - inline float GetProjSpeed(int idx) const { return m_gun[idx].projData.speed; }; - inline const vector3d &GetTargetLeadPos() const { return m_targetLeadPos; } - inline const vector3d &GetCurrentLeadDir() const { return m_currentLeadDir; } - inline bool IsFiringSolutionOk() const { return m_firingSolutionOk; } - inline void SetCoolingBoost(float cooler) { m_cooler_boost = cooler; }; - -private: - struct GunData { - struct GunLoc { - vector3d pos; - vector3d dir; - }; - std::vector locs; - float recharge; - float temp_heat_rate; - float temp_cool_rate; - bool dual; - ProjectileData projData; - }; - - bool m_is_firing[Guns::GUNMOUNT_MAX]; - float m_recharge_stat[Guns::GUNMOUNT_MAX]; - float m_temperature_stat[Guns::GUNMOUNT_MAX]; - //TODO: Make it a vector and rework struct Gun to have bool dir={Forward,Backward} - bool m_gun_present[Guns::GUNMOUNT_MAX]; - GunData m_gun[Guns::GUNMOUNT_MAX]; - float m_cooler_boost; - bool m_shouldUseLeadCalc = false; - vector3d m_targetLeadPos; - vector3d m_currentLeadDir; - bool m_firingSolutionOk = false; -}; - -#endif // FIXEDGUNS_H From 46c758e0585d702b57ad6c2ceaa425aa31bdd608 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 5 Dec 2024 14:23:30 -0500 Subject: [PATCH 12/17] Address review feedback - Improve documentation on several functions and types - Add boolean return value to AddWeaponMount - Convert internal functions to take state values by reference rather than pointer - Make defaults for WeaponData/WeaponState less specific --- src/ship/GunManager.cpp | 52 ++++++++++++++++++++++------------------- src/ship/GunManager.h | 36 ++++++++++++++++------------ 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp index acbae23e2ba..4340e55c344 100644 --- a/src/ship/GunManager.cpp +++ b/src/ship/GunManager.cpp @@ -37,7 +37,7 @@ void GunManager::LoadFromJson(const Json &jsonObj, Space *space) { } -void GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimit) +bool GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimit) { WeaponMount mount = {}; @@ -48,7 +48,8 @@ void GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimb tan(DEG2RAD(gimbalLimit.y)) ); - m_mounts.try_emplace(id, mount); + auto result = m_mounts.try_emplace(id, mount); + return result.second; } void GunManager::RemoveWeaponMount(StringName id) @@ -341,7 +342,7 @@ void GunManager::StaticUpdate(float deltaTime) } // bring position, velocity and acceleration into ship-space - CalcWeaponLead(&gun, relPosition * orient, relVelocity * orient, relAccel * orient); + CalcWeaponLead(gun, relPosition * orient, relVelocity * orient, relAccel * orient); } else { gun.currentLead = vector3f(0, 0, 1); gun.currentLeadPos = vector3d(0, 0, 0); @@ -369,7 +370,7 @@ void GunManager::StaticUpdate(float deltaTime) uint32_t numShots = 1 + floor((missedTime + deltaTime) / deltaShot); for (uint32_t i = 0; i < numShots; ++i) { - Fire(&gun, &gs); + Fire(gun); } // set the next fire time, making sure to preserve accumulated (fractional) shot time @@ -390,16 +391,16 @@ void GunManager::StaticUpdate(float deltaTime) m_isAnyFiring = isAnyFiring; } -void GunManager::Fire(WeaponState *weapon, GroupState *group) +void GunManager::Fire(WeaponState &weapon) { - WeaponData *data = &weapon->data; + WeaponData *data = &weapon.data; // either fire the next barrel in sequence or fire all at the same time size_t firstBarrel = 0; size_t numBarrels = 1; if (data->staggerBarrels || data->numBarrels == 1) { - firstBarrel = (weapon->lastBarrel + 1) % data->numBarrels; - weapon->lastBarrel = firstBarrel; + firstBarrel = (weapon.lastBarrel + 1) % data->numBarrels; + weapon.lastBarrel = firstBarrel; } else { numBarrels = data->numBarrels; } @@ -411,10 +412,10 @@ void GunManager::Fire(WeaponState *weapon, GroupState *group) const matrix3x3d &orient = m_parent->GetOrient(); // mount-relative aiming direction - const vector3d leadDir = vector3d(wpn_orient * weapon->currentLead).Normalized(); + const vector3d leadDir = vector3d(wpn_orient * weapon.currentLead).Normalized(); for (size_t idx = firstBarrel; idx < firstBarrel + numBarrels; idx++) { - weapon->temperature += data->firingHeat; + weapon.temperature += data->firingHeat; // TODO: get individual barrel locations from gun model and cache them const vector3d dir = orient * leadDir; const vector3d pos = orient * wpn_pos + m_parent->GetPosition(); @@ -429,16 +430,18 @@ void GunManager::Fire(WeaponState *weapon, GroupState *group) } // Note that position and relative velocity are in the coordinate system of the host body -void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity, vector3d relativeAccel) +void GunManager::CalcWeaponLead(WeaponState &state, vector3d position, vector3d relativeVelocity, vector3d relativeAccel) { // Compute the forward vector for the weapon mount + // NOTE: weapons by convention use +Z as their "forward" vector, + // as this is the most natural mapping to the content authoring pipeline for ships const matrix4x4f &xform = GetMountTransform(state); const vector3f forward = vector3f(0, 0, 1); - if (state->data.projectileType == PROJECTILE_BALLISTIC) { + if (state.data.projectileType == PROJECTILE_BALLISTIC) { // Calculate firing solution and relative velocity along our z axis by // computing the position along the enemy ship's lead vector at which to aim - const double projspeed = state->data.projectile.speed; + const double projspeed = state.data.projectile.speed; // Account for the distance between the weapon mount and the center of the parent position -= vector3d(xform.GetTranslate()); @@ -460,7 +463,7 @@ void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d //l = (-b + sqrt(delta)) / 2a; t=1/l; a>0 double t = 2 * a / (-b + sqrt(delta)); - if (t < 0 || t > state->data.projectile.lifespan) { + if (t < 0 || t > state.data.projectile.lifespan) { //no positive solution or target too far } else { //This is an exact solution as opposed to 2 step approximation used before. @@ -477,16 +480,16 @@ void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d //no solution } - state->currentLeadPos = leadPos; - } else if (state->data.projectileType == PROJECTILE_BEAM) { + state.currentLeadPos = leadPos; + } else if (state.data.projectileType == PROJECTILE_BEAM) { // Beam weapons should just aim at the target - state->currentLeadPos = position; + state.currentLeadPos = position; } // Transform the target's direction into the coordinate space of the mount, // with the barrel pointing "forward" towards +Z. // float has plenty of precision when working with normalized directions. - vector3f targetDir = vector3f(state->currentLeadPos.Normalized()) * xform.GetOrient(); + vector3f targetDir = vector3f(state.currentLeadPos.Normalized()) * xform.GetOrient(); // We represent the maximum traverse of the weapon as an ellipse relative // to the +Z axis of the mount. @@ -496,18 +499,19 @@ void GunManager::CalcWeaponLead(WeaponState *state, vector3d position, vector3d // vector. // Note that we scale the targetDir vector such that the z component has a length of 1.0, // so that the comparison with the tangent of the gimbal limit is correct. - vector2f traverseRel = (targetDir * (1.0 / targetDir.z)).xy() / state->mount->gimbalLimitTan; + vector2f traverseRel = (targetDir * (1.0 / targetDir.z)).xy() / state.mount->gimbalLimitTan; - state->withinGimbalLimit = targetDir.z > 0 && traverseRel.LengthSqr() <= 1.0; - state->currentLead = state->withinGimbalLimit ? targetDir : forward; + state.withinGimbalLimit = targetDir.z > 0 && traverseRel.LengthSqr() <= 1.0; + state.currentLead = state.withinGimbalLimit ? targetDir : forward; } +// Default matrix to use +Z as weapon forward static const matrix4x4f s_noMountTransform = matrix4x4f::RotateXMatrix(M_PI); -const matrix4x4f &GunManager::GetMountTransform(WeaponState *state) +const matrix4x4f &GunManager::GetMountTransform(WeaponState &state) { - if (state->mount->tag) { - return state->mount->tag->GetGlobalTransform(); + if (state.mount->tag) { + return state.mount->tag->GetGlobalTransform(); } return s_noMountTransform; diff --git a/src/ship/GunManager.h b/src/ship/GunManager.h index 2e98a6f4b4f..66c1c1f2df0 100644 --- a/src/ship/GunManager.h +++ b/src/ship/GunManager.h @@ -42,8 +42,8 @@ class GunManager : public LuaWrappable { }; struct ProjectileDef { - float lifespan = 5.0; - float speed = 1000.0; + float lifespan = 1; + float speed = 1; // Damage float impactDamage = 0.0; // How much damage is dealt when this projectile hits a target @@ -59,7 +59,7 @@ class GunManager : public LuaWrappable { // Fusing float proxyFuseRadius = 0.0; // This projectile will detonate when it detects a body within this range - float proxyFuseArmTime = 0.5; // How long after firing before the projectile fuse is armed? + float proxyFuseArmTime = 0.0; // How long after firing before the projectile fuse is armed? // Visual settings float length = 0; @@ -71,12 +71,12 @@ class GunManager : public LuaWrappable { // TODO: create one of these in Lua per weapon definition and reference them from each mounted gun // TODO: create a separate projectile definition and reference it struct WeaponData { - float firingRPM = 240; // number of shots per minute (60s / time between shots) - float firingHeat = 4.5; // amount of thermal energy(kJ) emitted into the system per shot + float firingRPM = 1; // number of shots per minute (60s / time between shots) + float firingHeat = 0; // amount of thermal energy(kJ) emitted into the system per shot //TODO: integrate this with a whole-ship cooling system - float coolingPerSecond = 14.2; // nominal amount of thermal energy removed per second (kW) - float overheatThreshold = 280; // total amount of thermal energy(kJ) the gun can store while functioning + float coolingPerSecond = 0; // nominal amount of thermal energy removed per second (kW) + float overheatThreshold = 1; // total amount of thermal energy(kJ) the gun can store while functioning std::string modelPath; // model to render this weapon with @@ -107,6 +107,8 @@ class GunManager : public LuaWrappable { SceneGraph::Model *model; // gun model, currently unused }; + // Information about a specific mount that a weapon is attached to + // Currently only handles gimballed weapon mounts, but may support turrets in the future struct WeaponMount { StringName id; SceneGraph::Tag *tag; // Tag in the parent model that this weapon mount is attached to @@ -115,6 +117,7 @@ class GunManager : public LuaWrappable { // TODO: enable/disable hardpoint based on ship configuration, i.e. landing/vtol/wings? }; + // Combines one or more weapons with a shared fire-control trigger and targeting information struct GroupState { WeaponIndexSet weapons; // Whic weapons are assigned to this group? const Body *target; // The target for this group, if any @@ -134,16 +137,19 @@ class GunManager : public LuaWrappable { // ========================================== - // Add a weapon mount to this gun manager - void AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimitDegrees); - // Remove a weapon mount from this gun manager. This will fail if a weapon is still mounted. + // Add a weapon mount to this gun manager. + // Returns false if a hardpoint already exists on this GunManager with the specified name. + bool AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimitDegrees); + // Remove a weapon mount from this gun manager. + // The caller should always ensure that the weapon mount is empty before calling this function. void RemoveWeaponMount(StringName id); - // Attach a weapon to a specific mount + // Attach a weapon to a specific mount. + // Returns false if the hardpoint cannot be found or the weapon could not be mounted. bool MountWeapon(StringName hardpoint, const WeaponData &data); // Remove the attached weapon from a specific mount void UnmountWeapon(StringName hardpoint); - // Check if a weapon is attached to a specific mount + // Check if any weapon is attached to a specific mount bool IsWeaponMounted(StringName hardpoint) const; const std::vector &GetWeapons() const { return m_weapons; } @@ -197,13 +203,13 @@ class GunManager : public LuaWrappable { // Handle checking and firing a given gun. // Note that this currently does not nicely handle spawning multiple projectiles per timestep - i.e. timewarp or a weapon RPM higher than 3600 // Projectile spawns are also "snapped" to the start of a timestep if they are not direct multiples of the timestep duration - void Fire(WeaponState *weapon, GroupState *group); + void Fire(WeaponState &weapon); // Calculate the position a given gun should aim at to hit the current target body // This is effectively the position of the target at T+n - void CalcWeaponLead(WeaponState *state, vector3d position, vector3d relativeVelocity, vector3d relativeAccel); + void CalcWeaponLead(WeaponState &state, vector3d position, vector3d relativeVelocity, vector3d relativeAccel); - const matrix4x4f &GetMountTransform(WeaponState *weapon); + const matrix4x4f &GetMountTransform(WeaponState &weapon); void RemoveGroupIndex(WeaponIndexSet &group, uint32_t index); From b5b06a704ca2fa0da932c519ee37ad4c41d5b387 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 5 Dec 2024 16:03:06 -0500 Subject: [PATCH 13/17] JsonUtils: add StringName<->JSON serialization --- src/JsonUtils.cpp | 11 +++++++++++ src/JsonUtils.h | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/src/JsonUtils.cpp b/src/JsonUtils.cpp index 3bbe4a2d8ed..67108ce764a 100644 --- a/src/JsonUtils.cpp +++ b/src/JsonUtils.cpp @@ -9,6 +9,7 @@ #include "FileSystem.h" #include "base64/base64.hpp" #include "core/GZipFormat.h" +#include "core/StringName.h" #include "utils.h" #include @@ -851,3 +852,13 @@ void StrToMatrix4x4d(const char *str, matrix4x4d &val) val[i] = fu[i].d; #endif } + +void to_json(Json &out, const StringName &str) +{ + out = str.sv(); +} + +void from_json(const Json &obj, StringName &str) +{ + str = StringName(obj.get()); +} diff --git a/src/JsonUtils.h b/src/JsonUtils.h index fc7a5ce3196..362a04cd328 100644 --- a/src/JsonUtils.h +++ b/src/JsonUtils.h @@ -18,6 +18,8 @@ #include "matrix4x4.h" #include "vector3.h" +class StringName; + namespace FileSystem { class FileSource; class FileData; @@ -117,4 +119,7 @@ void StrToMatrix3x3d(const char *str, matrix3x3d &val); void StrToMatrix4x4f(const char *str, matrix4x4f &val); void StrToMatrix4x4d(const char *str, matrix4x4d &val); +void to_json(Json &, const StringName &); +void from_json(const Json &, StringName &); + #endif /* _JSON_UTILS_H */ From e02a104dd040e5ce7e1f81e4d5c501457866afc6 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 5 Dec 2024 16:03:52 -0500 Subject: [PATCH 14/17] GunManager: add serialization code - Ugly but functional, as all C++ serialization code tends to be. --- src/ship/GunManager.cpp | 210 +++++++++++++++++++++++++++++++++++++++- src/ship/GunManager.h | 7 +- 2 files changed, 211 insertions(+), 6 deletions(-) diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp index 4340e55c344..dcaec1c9854 100644 --- a/src/ship/GunManager.cpp +++ b/src/ship/GunManager.cpp @@ -5,10 +5,12 @@ #include "Beam.h" #include "DynamicBody.h" -#include "Projectile.h" #include "Game.h" +#include "JsonUtils.h" #include "ModelBody.h" #include "Pi.h" +#include "Projectile.h" +#include "Space.h" #include "lua/LuaBodyComponent.h" #include "matrix4x4.h" #include "scenegraph/Tag.h" @@ -19,6 +21,20 @@ REGISTER_COMPONENT_TYPE(GunManager) { BodyComponentDB::RegisterLuaInterface(); } +// ============================================================================= + +// Forward-declared serialization code +void from_json(const Json &obj, GunManager::WeaponData &data); +void to_json(Json &out, const GunManager::WeaponData &data); +void from_json(const Json &obj, GunManager::WeaponState &weapon); +void to_json(Json &out, const GunManager::WeaponState &weapon); +void from_json(const Json &obj, GunManager::WeaponMount &mount); +void to_json(Json &out, const GunManager::WeaponMount &mount); +void from_json(const Json &obj, GunManager::GroupState &group); +void to_json(Json &out, const GunManager::GroupState &group); + +// ============================================================================= + void GunManager::Init(ModelBody *b) { m_parent = b; @@ -29,12 +45,77 @@ void GunManager::Init(ModelBody *b) } } -void GunManager::SaveToJson(Json &jsonObj, Space *space) +void GunManager::SaveToJson(Json &out, Space *space) { + out["isAnyFiring"] = m_isAnyFiring; + out["coolingBoost"] = m_coolingBoost; + + Json &mounts = (out["mounts"] = Json::object()); + for (auto &pair : m_mounts) { + mounts.emplace(pair.first, pair.second); + } + + Json &weapons = (out["weapons"] = Json::array()); + for (auto &weapon : m_weapons) { + weapons.push_back(weapon); + } + + Json &groups = (out["groups"] = Json::array()); + for (auto &group : m_groups) { + groups.push_back(group); + + if (group.target) { + groups.back()["target"] = space->GetIndexForBody(group.target); + } + } } void GunManager::LoadFromJson(const Json &jsonObj, Space *space) { + m_isAnyFiring = jsonObj.value("isAnyFiring", false); + m_coolingBoost = jsonObj.value("coolingBoost", 1.0); + + const Json &mounts = jsonObj["mounts"]; + for (const auto &pair : mounts.items()) { + WeaponMount mount = pair.value().get(); + + // Find the tag for this weapon mount + mount.tag = m_parent->GetModel()->FindTagByName(pair.value()["tag"]); + + m_mounts.try_emplace(std::string_view(pair.key()), std::move(mount)); + } + + const Json &weapons = jsonObj["weapons"]; + for (const auto &weapon : weapons) { + m_weapons.push_back(weapon.get()); + + // Fixup the mount pointer + StringName mount = weapon["mount"].get(); + m_weapons.back().mount = &m_mounts[mount]; + } + + m_groups.clear(); + + const Json &groups = jsonObj["groups"]; + for (const auto &group : groups) { + m_groups.push_back(group.get()); + + if (group.count("target")) { + + size_t groupIdx = m_groups.size() - 1; + size_t targetIdx = group["target"].get(); + + Body *target = space->GetBodyByIndex(targetIdx); + + if (!target) { + Log::Warning("Could not find target body index {} for ship {} (weapon group {})", + targetIdx, m_parent->GetLabel(), groupIdx); + continue; + } + + SetGroupTarget(groupIdx, target); + } + } } bool GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimit) @@ -516,3 +597,128 @@ const matrix4x4f &GunManager::GetMountTransform(WeaponState &state) return s_noMountTransform; } + +// ============================================================================= + +// JSON serialization functions + +void from_json(const Json &obj, GunManager::WeaponData &data) +{ + data.firingRPM = obj.value("rpm", 1.f); + data.firingHeat = obj.value("heat", 0.f); + data.coolingPerSecond = obj.value("cooling", 0.f); + data.overheatThreshold = obj.value("overheat", 1.f); + + data.projectileType = GunManager::ProjectileType(obj.value("type", 0)); + data.numBarrels = obj.value("numBarrels", 1); + data.staggerBarrels = obj.value("staggerBarrels", false); + + data.modelPath = obj.value("modelPath", std::string()); + + data.projectile = {}; + + if (const Json &pr = obj.value("projectile", Json()); pr.is_object()) { + ProjectileData &out = data.projectile; + out.lifespan = pr.value("lifespan", 0.f); + out.damage = pr.value("damage", 0.f); + out.length = pr.value("length", 0.f); + out.width = pr.value("width", 0.f); + out.speed = pr.value("speed", 0.f); + out.color = pr.value("color", Color()); + out.mining = pr.value("mining", false); + out.beam = pr.value("beam", false); + } +} + +void to_json(Json &out, const GunManager::WeaponData &data) +{ + out["rpm"] = data.firingRPM; + out["heat"] = data.firingHeat; + out["cooling"] = data.coolingPerSecond; + out["overheat"] = data.overheatThreshold; + + out["type"] = int(data.projectileType); + out["numBarrels"] = data.numBarrels; + out["staggerBarrels"] = data.staggerBarrels; + + if (!data.modelPath.empty()) { + out["modelPath"] = data.modelPath; + } + + Json &outProj = (out["projectile"] = Json::object()); + const ProjectileData &pr = data.projectile; + + outProj["lifespan"] = pr.lifespan; + outProj["damage"] = pr.damage; + outProj["length"] = pr.length; + outProj["width"] = pr.width; + outProj["speed"] = pr.speed; + outProj["color"] = pr.color; + outProj["mining"] = pr.mining; + outProj["beam"] = pr.beam; +} + +void from_json(const Json &obj, GunManager::WeaponState &weapon) +{ + weapon.group = obj.value("group", 0); + weapon.lastBarrel = obj.value("lastBarrel", 0); + weapon.temperature = obj.value("temperature", 0.f); + weapon.nextFireTime = obj.value("nextFireTime", 0.0); + weapon.data = obj["data"].get(); +} + +void to_json(Json &out, const GunManager::WeaponState &weapon) +{ + out["mount"] = weapon.mount->id; + out["group"] = weapon.group; + out["lastBarrel"] = weapon.lastBarrel; + out["temperature"] = weapon.temperature; + out["nextFireTime"] = weapon.nextFireTime; + + // All lead calculation variables are transient and will be recreated on next timestep + // Weapon model is reconstructed from WeaponData + out["data"] = weapon.data; +} + +void from_json(const Json &obj, GunManager::WeaponMount &mount) +{ + mount.id = obj["id"].get(); + // mount tag will be fixed up on load + mount.tag = nullptr; + mount.gimbalLimitTan = obj["gimbalLimit"].get(); +} + +void to_json(Json &out, const GunManager::WeaponMount &mount) +{ + out["id"] = mount.id; + out["tag"] = mount.tag ? mount.tag->GetName() : ""; + out["gimbalLimit"] = mount.gimbalLimitTan; +} + +void from_json(const Json &obj, GunManager::GroupState &group) +{ + if (const Json &weapons = obj.value("weapons", Json()); weapons.is_array()) { + for (const auto &index : weapons) { + group.weapons[index.get()] = true; + } + } + + // Target will be fixed up externally + group.target = nullptr; + group.firing = obj.value("firing", false); + group.fireWithoutTargeting = obj.value("fireWithoutTargeting", false); +} + +void to_json(Json &out, const GunManager::GroupState &group) +{ + Json &weapons = (out["weapons"] = Json::array()); + for (size_t idx = 0; idx < group.weapons.size(); idx++) { + if (group.weapons[idx]) { + weapons.push_back(idx); + } + } + + // Target is added externally, needs a reference to Space + out["firing"] = group.firing; + out["fireWithoutTargeting"] = group.fireWithoutTargeting; +} diff --git a/src/ship/GunManager.h b/src/ship/GunManager.h index 66c1c1f2df0..a8bf261534d 100644 --- a/src/ship/GunManager.h +++ b/src/ship/GunManager.h @@ -78,12 +78,12 @@ class GunManager : public LuaWrappable { float coolingPerSecond = 0; // nominal amount of thermal energy removed per second (kW) float overheatThreshold = 1; // total amount of thermal energy(kJ) the gun can store while functioning - std::string modelPath; // model to render this weapon with - - ProjectileData projectile; // deprecated, to replace with RefCountedPtr ProjectileType projectileType = PROJECTILE_BALLISTIC; uint8_t numBarrels = 1; // total number of barrels on the model bool staggerBarrels = false; // should we fire one barrel after another, or both at the same time? + + ProjectileData projectile; // deprecated, to replace with RefCountedPtr + std::string modelPath; // model to render this weapon with }; // Information about a specific mounted weapon (serialized directly) @@ -221,7 +221,6 @@ class GunManager : public LuaWrappable { WeaponIndexSet m_stoppedThisFrame; WeaponIndexSet m_stoppedNextFrame; - ConnectionTicket m_targetDestroyedCallback; ModelBody *m_parent = nullptr; bool m_isAnyFiring = false; float m_coolingBoost = 1.0; From 3d6e0cfe70701337ac4287df5a770d85c63c5acc Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 5 Dec 2024 16:04:58 -0500 Subject: [PATCH 15/17] radar.lua: fix error when playing multiple games in the same session - Because the player variable is cached at file scope, it becomes orphaned on end game. - Reset it during cleanup phase so it can be populated with the correct Game.player object during the next game. --- data/pigui/modules/radar.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/pigui/modules/radar.lua b/data/pigui/modules/radar.lua index e30e140ecf8..82fa17cff78 100644 --- a/data/pigui/modules/radar.lua +++ b/data/pigui/modules/radar.lua @@ -506,6 +506,7 @@ end) -- reset radar to default at game end Event.Register("onGameEnd", function() + player = nil shouldDisplay2DRadar = false radar2d:resetZoom() radar3d:resetZoom() From 2266dcf5965db4c9f7aba2eb71aa58342ce98b28 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Thu, 5 Dec 2024 20:02:09 -0500 Subject: [PATCH 16/17] Better error handling for Add/RemoveWeaponMount --- data/libs/Ship.lua | 8 ++++++-- src/ship/GunManager.cpp | 7 ++++--- src/ship/GunManager.h | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/data/libs/Ship.lua b/data/libs/Ship.lua index b1858ab7444..99e3fb25b95 100644 --- a/data/libs/Ship.lua +++ b/data/libs/Ship.lua @@ -53,11 +53,15 @@ function Ship:UpdateWeaponSlots() for _, slot in ipairs(equipSet:GetAllSlotsOfType("weapon", true)) do if not slot.gimbal then - print('Missing hardpoint gimbal on ship {} for slot {}' % { self.shipId, slot.id }) + logWarning('Missing hardpoint gimbal on ship {} for slot {}' % { self.shipId, slot.id }) end local gimbal = Vector2(table.unpack(slot.gimbal or { 1, 1 })) - gunManager:AddWeaponMount(slot.id, slot.tag, gimbal) + local ok = gunManager:AddWeaponMount(slot.id, slot.tag, gimbal) + + if not ok then + logWarning('Unable to add weapon mount slot {} on ship {}' % { slot.id, self.shipId }) + end end end diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp index dcaec1c9854..b00d9666139 100644 --- a/src/ship/GunManager.cpp +++ b/src/ship/GunManager.cpp @@ -133,16 +133,17 @@ bool GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimb return result.second; } -void GunManager::RemoveWeaponMount(StringName id) +bool GunManager::RemoveWeaponMount(StringName id) { auto iter = m_mounts.find(id); if (iter == m_mounts.end()) - return; + return false; if (IsWeaponMounted(id)) - return; + return false; m_mounts.erase(iter); + return true; } bool GunManager::MountWeapon(StringName hardpoint, const WeaponData &gunData) diff --git a/src/ship/GunManager.h b/src/ship/GunManager.h index a8bf261534d..4be57ef8d5a 100644 --- a/src/ship/GunManager.h +++ b/src/ship/GunManager.h @@ -142,7 +142,8 @@ class GunManager : public LuaWrappable { bool AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimitDegrees); // Remove a weapon mount from this gun manager. // The caller should always ensure that the weapon mount is empty before calling this function. - void RemoveWeaponMount(StringName id); + // Returns false if the mount does not exist or is not empty. + bool RemoveWeaponMount(StringName id); // Attach a weapon to a specific mount. // Returns false if the hardpoint cannot be found or the weapon could not be mounted. From e78994be02ff8bdd110e8b4f5f2c858f2b1e1597 Mon Sep 17 00:00:00 2001 From: Webster Sheets Date: Fri, 6 Dec 2024 21:34:37 -0500 Subject: [PATCH 17/17] GunManager: take StringName by reference - The StringName values are borrowed rather than copied, which saves a small fixed cost to increment the refcount --- src/ship/GunManager.cpp | 12 ++++++------ src/ship/GunManager.h | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ship/GunManager.cpp b/src/ship/GunManager.cpp index b00d9666139..033fa0c392b 100644 --- a/src/ship/GunManager.cpp +++ b/src/ship/GunManager.cpp @@ -118,7 +118,7 @@ void GunManager::LoadFromJson(const Json &jsonObj, Space *space) } } -bool GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimit) +bool GunManager::AddWeaponMount(const StringName &id, const StringName &tagName, vector2f gimbalLimit) { WeaponMount mount = {}; @@ -133,7 +133,7 @@ bool GunManager::AddWeaponMount(StringName id, StringName tagName, vector2f gimb return result.second; } -bool GunManager::RemoveWeaponMount(StringName id) +bool GunManager::RemoveWeaponMount(const StringName &id) { auto iter = m_mounts.find(id); if (iter == m_mounts.end()) @@ -146,7 +146,7 @@ bool GunManager::RemoveWeaponMount(StringName id) return true; } -bool GunManager::MountWeapon(StringName hardpoint, const WeaponData &gunData) +bool GunManager::MountWeapon(const StringName &hardpoint, const WeaponData &gunData) { auto iter = m_mounts.find(hardpoint); if (iter == m_mounts.end()) @@ -173,7 +173,7 @@ bool GunManager::MountWeapon(StringName hardpoint, const WeaponData &gunData) return true; } -void GunManager::UnmountWeapon(StringName hardpoint) +void GunManager::UnmountWeapon(const StringName &hardpoint) { auto iter = std::find_if(m_weapons.begin(), m_weapons.end(), [&](const WeaponState &ws) { return ws.mount->id == hardpoint; @@ -192,7 +192,7 @@ void GunManager::UnmountWeapon(StringName hardpoint) } } -bool GunManager::IsWeaponMounted(StringName hardpoint) const +bool GunManager::IsWeaponMounted(const StringName &hardpoint) const { return std::find_if(m_weapons.begin(), m_weapons.end(), [&](const WeaponState &ws) { return ws.mount->id == hardpoint; @@ -210,7 +210,7 @@ void GunManager::RemoveGroupIndex(WeaponIndexSet &group, uint32_t index) group = (group & keep) | ((group & mask) >> 1); } -uint32_t GunManager::GetWeaponIndexForHardpoint(StringName hardpoint) const +uint32_t GunManager::GetWeaponIndexForHardpoint(const StringName &hardpoint) const { auto iter = std::find_if(m_weapons.begin(), m_weapons.end(), [&](const WeaponState &ws) { return ws.mount->id == hardpoint; diff --git a/src/ship/GunManager.h b/src/ship/GunManager.h index 4be57ef8d5a..b660d4940fd 100644 --- a/src/ship/GunManager.h +++ b/src/ship/GunManager.h @@ -139,25 +139,25 @@ class GunManager : public LuaWrappable { // Add a weapon mount to this gun manager. // Returns false if a hardpoint already exists on this GunManager with the specified name. - bool AddWeaponMount(StringName id, StringName tagName, vector2f gimbalLimitDegrees); + bool AddWeaponMount(const StringName &id, const StringName &tagName, vector2f gimbalLimitDegrees); // Remove a weapon mount from this gun manager. // The caller should always ensure that the weapon mount is empty before calling this function. // Returns false if the mount does not exist or is not empty. - bool RemoveWeaponMount(StringName id); + bool RemoveWeaponMount(const StringName &id); // Attach a weapon to a specific mount. // Returns false if the hardpoint cannot be found or the weapon could not be mounted. - bool MountWeapon(StringName hardpoint, const WeaponData &data); + bool MountWeapon(const StringName &hardpoint, const WeaponData &data); // Remove the attached weapon from a specific mount - void UnmountWeapon(StringName hardpoint); + void UnmountWeapon(const StringName &hardpoint); // Check if any weapon is attached to a specific mount - bool IsWeaponMounted(StringName hardpoint) const; + bool IsWeaponMounted(const StringName &hardpoint) const; const std::vector &GetWeapons() const { return m_weapons; } uint32_t GetNumWeapons() const { return m_weapons.size(); } const WeaponState *GetWeaponState(uint32_t numWeapon) const { return m_weapons.size() > numWeapon ? &m_weapons[numWeapon] : nullptr; } - uint32_t GetWeaponIndexForHardpoint(StringName hardpoint) const; + uint32_t GetWeaponIndexForHardpoint(const StringName &hardpoint) const; // ==========================================