Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 266 additions & 1 deletion docs/backlog/issues.md

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions docs/db_structure/migrations/ISSUE-042-hunter-drone-faction.sql
Comment thread
Sellafield marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- ISSUE-042: Hunter Drone chassis definitions are missing two options that every other
-- player-deployed drone chassis already sets, both fixed here.
--
-- === Part 1: NPCs never aggro Hunter Drones (item 3) ===
--
-- Root cause: EntityDefaultOptions.Faction (src/Perpetuum/EntityFramework/EntityDefaultOptions.cs:151-159)
-- falls back to Faction.Niani when the `faction` key is absent from a definition's options string:
-- string typeString = _dictionary.GetOrDefault<string>("faction");
-- return typeString != null ? (Faction)Enum.Parse(typeof(Faction), typeString) : Faction.Niani;
-- def_standard_hunter_drone_pve and def_standard_hunter_drone_pvp both had options = NULL (no
-- faction key at all), so both silently resolve to Faction.Niani -- the very faction the PvE
-- variant is designed to hunt.
--
-- BodyPullThreatHelper.ProcessNpcThreats (src/Perpetuum/Zones/NpcSystem/AI/BodyPullThreatHelper.cs:149-156)
-- skips adding threat whenever the scanning NPC's faction equals the target's:
-- if (smartCreature.Behavior.Type != BehaviorType.RemoteControlledTurret &&
-- smartCreature.ED.Options.Faction == unit.ED.Options.Faction)
-- { return; }
-- so any Niani-faction NPC treats a Hunter Drone as friendly and never retaliates.
--
-- Every other player-deployed drone chassis already carries an explicit faction option -- verified
-- against def_nuimqol_assault_drone, def_pelistal_assault_drone, def_repair_support_drone,
-- def_mining_industrial_drone and def_harvesting_industrial_drone, all `#faction=sSyndicate`.
--
-- === Part 2: Recalled Hunter Drones never leave the zone (found investigating item 2) ===
--
-- RemoteControlledCreature.Scoop() (src/Perpetuum/Zones/RemoteControl/RemoteControlledCreature.cs:69-89)
-- only calls RemoveFromZone() inside `if (ED.Options.PackedTurretId != 0)`. EntityDefaultOptions.PackedTurretId
-- (EntityDefaultOptions.cs:130-134) defaults to 0 when the `packedTurretId` key is absent -- which it
-- was for both hunter chassis rows -- so a drone that successfully retreats to guard range and calls
-- Scoop() (HunterRetreatAI.cs:67) would silently do nothing and sit there forever.
--
-- This is NOT missing content: def_standard_hunter_drone_rcu_pve (8978) and
-- def_standard_hunter_drone_rcu_pvp (8979) are the same dual-purpose ammo items every other drone
-- type uses -- consumed on deploy AND returned to cargo on recall. Confirmed against the existing,
-- working pointer pair for def_nuimqol_assault_drone: chassis (8603) has `packedTurretId=i219c`
-- pointing at its ammo unit (8604), and that ammo unit has `turretId=i219b` pointing back at the
-- chassis -- a bidirectional pair. The hunter RCU items already carry the forward half
-- (`def_standard_hunter_drone_rcu_pve.turretId=i230f` -> chassis 8975,
-- `def_standard_hunter_drone_rcu_pvp.turretId=i2310` -> chassis 8976); only the chassis side's
-- `packedTurretId` pointing back was missing. 8978 = 0x2312, 8979 = 0x2313 (confirmed via SQL
-- Server's own FORMAT(), not hand arithmetic).
--
-- Safe to re-run: each WHERE guard makes its statement a no-op once the option is already present.

UPDATE dbo.entitydefaults
SET options = COALESCE(options, '') + '#faction=sSyndicate'
WHERE definitionname IN ('def_standard_hunter_drone_pve', 'def_standard_hunter_drone_pvp')
AND (options IS NULL OR options NOT LIKE '%faction=%');

UPDATE dbo.entitydefaults
SET options = COALESCE(options, '') + '#packedTurretId=i2312'
WHERE definitionname = 'def_standard_hunter_drone_pve'
AND (options IS NULL OR options NOT LIKE '%packedTurretId=%');

UPDATE dbo.entitydefaults
SET options = COALESCE(options, '') + '#packedTurretId=i2313'
WHERE definitionname = 'def_standard_hunter_drone_pvp'
AND (options IS NULL OR options NOT LIKE '%packedTurretId=%');

-- Verification: both rows should now show options containing both '#faction=sSyndicate' and
-- '#packedTurretId=i2312' / '#packedTurretId=i2313' respectively.
-- SELECT definition, definitionname, options FROM dbo.entitydefaults
-- WHERE definitionname IN ('def_standard_hunter_drone_pve', 'def_standard_hunter_drone_pvp');
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Perpetuum.EntityFramework;
using Perpetuum.ExportedTypes;
using Perpetuum.Items;
using Perpetuum.Modules.ModuleProperties;
using Perpetuum.Zones.Effects;
using Perpetuum.Zones.NpcSystem;
Expand All @@ -25,10 +26,19 @@ public HunterRemoteControllerModule(CategoryFlags ammoCategoryFlags) : base(ammo

protected override void SetupEffect(EffectBuilder effectBuilder)
{
// Deliberately empty: hunter drones have no amplifiable combat stats (their only
// mechanic is contact self-destruct via SelfDestructDetonation), so no drone_amplification
// -style effect is needed. Left as an explicit override to document this as a deliberate
// decision, not an accidental EffectType.undefined effect application.
double armorMaxModifier = GetPropertyModifier(AggregateField.drone_amplification_armor_max_modifier).Value;
double coreMaxModifier = GetPropertyModifier(AggregateField.drone_amplification_core_max_modifier).Value;
double coreRechargeTimeModifier = GetPropertyModifier(AggregateField.drone_amplification_core_recharge_time_modifier).Value;
double speedMaxModifier = GetPropertyModifier(AggregateField.drone_amplification_speed_max_modifier).Value;
double reactorRadiationModifier = GetPropertyModifier(AggregateField.drone_amplification_reactor_radiation_modifier).Value;

_ = effectBuilder
.SetType(EffectType.drone_amplification)
.WithPropertyModifier(new ItemPropertyModifier(AggregateField.drone_amplification_armor_max_modifier, AggregateFormula.Modifier, armorMaxModifier))
.WithPropertyModifier(new ItemPropertyModifier(AggregateField.drone_amplification_core_max_modifier, AggregateFormula.Modifier, coreMaxModifier))
.WithPropertyModifier(new ItemPropertyModifier(AggregateField.drone_amplification_core_recharge_time_modifier, AggregateFormula.Inverse, coreRechargeTimeModifier))
.WithPropertyModifier(new ItemPropertyModifier(AggregateField.drone_amplification_speed_max_modifier, AggregateFormula.Modifier, speedMaxModifier))
.WithPropertyModifier(new ItemPropertyModifier(AggregateField.drone_amplification_reactor_radiation_modifier, AggregateFormula.Inverse, reactorRadiationModifier));
}

public override RemoteControlledCreature CreateAndConfigureRcu(RemoteControlledUnit ammo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ protected override void OnStateChanged(IState state)

if (moduleState.Type == ModuleStateType.Idle)
{
RemoteControllerModule remoteController =
(RemoteControllerModule)ParentRobot?.ActiveModules.FirstOrDefault(x => x is RemoteControllerModule);

if (remoteController != null)
// A robot can carry more than one RemoteControllerModule fitted at once (e.g. a
// hunter drone controller alongside an ordinary drone controller), so the effect
// must be cleared from every one of them, not just the first found (ISSUE-042).
foreach (RemoteControllerModule remoteController in ParentRobot?.ActiveModules.OfType<RemoteControllerModule>() ?? Enumerable.Empty<RemoteControllerModule>())
{
foreach (Unit drone in remoteController.ActiveDrones)
{
Expand All @@ -71,12 +71,15 @@ protected override void OnStateChanged(IState state)

protected override void OnAction()
{
RemoteControllerModule remoteController =
(RemoteControllerModule)ParentRobot?.ActiveModules.FirstOrDefault(x => x is RemoteControllerModule);
double operationalRange = remoteController?.OperationalRange ?? 0;

if (remoteController != null)
// A robot can carry more than one RemoteControllerModule fitted at once (e.g. a
// hunter drone controller alongside an ordinary drone controller), so the translated
// command must be applied to every one of them, not just the first found (ISSUE-042).
// Each controller's own OperationalRange is used for its own drones, rather than
// reusing whichever controller happened to be picked first.
foreach (RemoteControllerModule remoteController in ParentRobot?.ActiveModules.OfType<RemoteControllerModule>() ?? Enumerable.Empty<RemoteControllerModule>())
{
double operationalRange = remoteController.OperationalRange;

foreach (Unit drone in remoteController.ActiveDrones)
{
OnApplyingEffect(drone);
Expand Down
7 changes: 5 additions & 2 deletions src/Perpetuum/Modules/RemoteControl/RemoteControllerModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,11 @@ protected override void OnAction()
remoteControlledCreature.AddToZone(Zone, position, ZoneEnterType.Default, deployBeamBuilder);
EffectBuilder effectBuilder = remoteControlledCreature.NewEffectBuilder();
SetupEffect(effectBuilder);
_ = effectBuilder.WithToken(_token);
remoteControlledCreature.ApplyEffect(effectBuilder);
if (effectBuilder.Type != EffectType.undefined)
{
_ = effectBuilder.WithToken(_token);
remoteControlledCreature.ApplyEffect(effectBuilder);
}
ConsumeAmmo();
}

Expand Down
10 changes: 6 additions & 4 deletions src/Perpetuum/Robots/Robot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,11 +405,13 @@ protected override bool IsDetected(Unit target)

protected override void OnBeforeRemovedFromZone(IZone zone)
{
Module remoteController = Modules?.FirstOrDefault(x => x is RemoteControllerModule);

if (remoteController != null)
// A robot can carry more than one RemoteControllerModule fitted at once (e.g. a
// hunter drone controller alongside an ordinary drone controller), each with its own
// BandwidthHandler/channels, so every one of them must be closed here — not just the
// first found (ISSUE-042).
foreach (RemoteControllerModule remoteController in Modules.OfType<RemoteControllerModule>())
{
(remoteController as RemoteControllerModule).CloseAllChannels();
remoteController.CloseAllChannels();
}

base.OnBeforeRemovedFromZone(zone);
Expand Down
2 changes: 2 additions & 0 deletions src/Perpetuum/Zones/Effects/EffectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public EffectBuilder(EffectFactory effectFactory)

public Unit Owner { get; private set; }

public EffectType Type => _type;

public EffectBuilder SetOwnerToSource()
{
_source = Owner;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ namespace Perpetuum.Zones.NpcSystem.AI.HunterDrones
/// </summary>
public class HunterSelfDestructAI : HunterDroneAI
{
private const double LeashRange = 50;
// Raw position units are x10'd to real in-game meters (ISSUE-042 item 4), so 5 here is
// the ~50m tether spec decision 12 actually asks for ("staying within 50m of the
// target") — not 50, which would be ~500m and never bind within an 8s countdown against
// any realistically paced target.
private const double LeashRange = 5;

// Guards against ED.Config.ActionDelay being misconfigured to zero (or a negative
// value) in the DB — mirrors SelfDestructModule.OnAction's fallback (see
Expand Down
Loading