Skip to content
Closed
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
14 changes: 8 additions & 6 deletions ValheimPlus/Configurations/Sections/TurretConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
{
public class TurretConfiguration : ServerSyncConfig<TurretConfiguration>
{
public bool ignorePlayers { get; internal set; } = false;
public bool disablePvP { get; internal set; } = false;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately I don't think we can get rid of an old config like this. Can we keep ignorePlayers and also add the new disablePvP config?

public bool unlimitedAmmo { get; internal set; } = false;
public float turnRate { get; internal set; } = 0;
public float attackCooldown { get; internal set; } = 0;
public float viewDistance { get; internal set; } = 0;
public float projectileVelocity { get; internal set; } = 0f;
public float projectileAccuracy { get; internal set; } = 0f;
Comment on lines -10 to -11
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove these?

public float turnRate { get; internal set; } = 22.5f;
public float attackCooldown { get; internal set; } = 2f;
public float viewDistance { get; internal set; } = 45f;
Comment on lines +7 to +9
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't change how these are calculated, all currently existing values would be invalid and do wild things. Stick to the modifiers.

We also don't want to use the game's values in case the devs tweak them, because then we would have to keep our values up to date with theirs.

public bool targetTamed { get; internal set; } = false;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should be true in the vanilla game

public float horizontalAngle { get; internal set; } = 50f;
public float verticalAngle { get; internal set; } = 50f;
Comment on lines +11 to +12
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use 0f as a sentinel value for "use default", and then otherwise use the given value here.

public bool fixProjectiles { get; internal set; } = false;
}
}
49 changes: 49 additions & 0 deletions ValheimPlus/GameClasses/BaseAI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using HarmonyLib;
using System.Collections.Generic;
using System.Reflection.Emit;
using UnityEngine;
using ValheimPlus.Configurations;

namespace ValheimPlus.GameClasses
{
[HarmonyPatch(typeof(BaseAI), nameof(BaseAI.FindClosestCreature))]
public static class BaseAI_FindClosestCreature_Patch
{
private static bool Turret_IsValidTarget(Transform me, Character target)
{
// Assume `targetPlayers` was true
var turret = me.GetComponent<Turret>();
if (turret == null || target is not Player p) return true;
var creator = TurretHelpers.GetPlayerCreator(turret);
return creator != p && p.IsPVPEnabled();
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should targetTurretCreator be a config for the first half of this &&? Based on vanilla, a PVP enabled turret should probably target its creator.

}

private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator ilGenerator)
{
var matcher = new CodeMatcher(instructions, ilGenerator);

if (Configuration.Current.Turret.IsEnabled && !Configuration.Current.Turret.disablePvP)
{
var Turret_IsValidTargetMethod = AccessTools.Method(typeof(BaseAI_FindClosestCreature_Patch), nameof(Turret_IsValidTarget));
var CharacterIsTamed = AccessTools.Method(typeof(Character), nameof(Character.IsTamed));

var continueLabel = matcher.MatchStartForward(
OpCodes.Brtrue,
new(inst => inst.IsLdarg(11)),
OpCodes.Brfalse
).ThrowIfNotMatch("Could not find `onlyTargets` in BaseAI.FindClosestCreature transpiler")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure these throws are wrapped so they don't propagate and break the entire mod. See other places where CodeMatcher is used for an example.

.Operand;

matcher.Advance(1).InsertAndAdvance(
new(OpCodes.Ldarg_0), // Transform
new(OpCodes.Ldloc, 5), // target
new(OpCodes.Call, Turret_IsValidTargetMethod),
new(OpCodes.Brfalse, continueLabel)
);
}

return matcher.InstructionEnumeration();
}
}

}
25 changes: 25 additions & 0 deletions ValheimPlus/GameClasses/Projectile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using HarmonyLib;
using ValheimPlus.Configurations;

namespace ValheimPlus.GameClasses
{
[HarmonyPatch(typeof(Projectile), nameof(Projectile.IsValidTarget))]
public static class Projectile_IsValidTarget_Patch
{
private static void Postfix(IDestructible destr, Projectile __instance, ref bool __result)
{
var turretConfig = Configuration.Current.Turret;
if (!turretConfig.IsEnabled || !turretConfig.fixProjectiles) return;

if (__result && destr is Turret turret)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My IDE flags this saying that Turret isn't an IDestructible, are you sure this works? Is it supposed to be like a .GetComponent<IDestructible>() on some GameObject? If your log is going off then I guess this is working but I would not know how.

{
var turretOwner = TurretHelpers.GetPlayerCreator(turret);
if (turretOwner == __instance.m_owner)
{
ValheimPlusPlugin.Logger.LogInfo("Turret projectile hit itself");
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove log statements like this if you are confident it is working.

__result = false;
}
}
}
}
}
143 changes: 72 additions & 71 deletions ValheimPlus/GameClasses/Turret.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,52 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using JetBrains.Annotations;
using UnityEngine;
using ValheimPlus.Configurations;

namespace ValheimPlus.GameClasses
{
public static class TurretHelpers
{
public static Player GetPlayerCreator(Turret turret)
{
var piece = turret.GetComponent<Piece>();
if (piece == null) return null;
return Player.GetPlayer(piece.GetCreator());
}

public static bool IsCreatorPvP(Turret turret)
=> GetPlayerCreator(turret)?.IsPVPEnabled() ?? false;
}

[HarmonyPatch(typeof(Turret), nameof(Turret.Awake))]
public static class Turret_Awake_Patch
{
/// <summary>
/// Configure the turret on wakeup
/// </summary>
[UsedImplicitly]
private static void Prefix(Turret __instance)
private static void Postfix(Turret __instance)
{
var config = Configuration.Current.Turret;
if (!config.IsEnabled) return;
if (config.ignorePlayers) __instance.m_targetPlayers = false;
__instance.m_turnRate = Helper.applyModifierValue(__instance.m_turnRate, config.turnRate);
__instance.m_attackCooldown = Helper.applyModifierValue(__instance.m_attackCooldown, config.attackCooldown);
__instance.m_viewDistance = Helper.applyModifierValue(__instance.m_viewDistance, config.viewDistance);
__instance.m_targetPlayers = !config.disablePvP && TurretHelpers.IsCreatorPvP(__instance);
__instance.m_targetTamed = config.targetTamed;
__instance.m_horizontalAngle = Mathf.Min(config.horizontalAngle, 180f);
__instance.m_verticalAngle = Mathf.Min(config.verticalAngle, 90f);
Comment on lines +37 to +38
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should clamp the minimum to 0 as well, no?

__instance.m_attackCooldown = config.attackCooldown;
__instance.m_viewDistance = config.viewDistance;
__instance.m_turnRate = config.turnRate;

// Change de/acceleration proportional to turnRate
// to maintain the degrees/second and normal game accuracy
float accelCoeff = 1.2f / 22.5f;
__instance.m_lookAcceleration = Mathf.Max(1.2f, accelCoeff * config.turnRate);

float deaccelCoeff = 0.05f / 22.5f;
__instance.m_lookDeacceleration = Mathf.Max(0.05f, deaccelCoeff * config.turnRate);
}
}

Expand All @@ -32,83 +56,60 @@ public static class Turret_ShootProjectile_Patch
private static readonly FieldInfo Field_Turret_M_MaxAmmo =
AccessTools.Field(typeof(Turret), nameof(Turret.m_maxAmmo));

private static readonly FieldInfo Field_Attack_M_ProjectileVel =
AccessTools.Field(typeof(Attack), nameof(Attack.m_projectileVel));

private static readonly FieldInfo Field_Attack_M_ProjectileAccuracy =
AccessTools.Field(typeof(Attack), nameof(Attack.m_projectileAccuracy));

[UsedImplicitly]
[HarmonyTranspiler]
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator ilGenerator)
{
var config = Configuration.Current.Turret;
if (!config.IsEnabled) return instructions;
if (!config.IsEnabled || !config.unlimitedAmmo) return instructions;

var unlimitedAmmoEnabled = config.unlimitedAmmo;
var projectileVelocityEnabled = config.projectileVelocity != 0f;
var projectileAccuracyEnabled = config.projectileAccuracy != 0f;
if (!unlimitedAmmoEnabled && !projectileVelocityEnabled && !projectileAccuracyEnabled) return instructions;
return new CodeMatcher(instructions, ilGenerator)
.MatchEndForward(
OpCodes.Ldarg_0,
new CodeMatch(inst => inst.LoadsField(Field_Turret_M_MaxAmmo)),
OpCodes.Ldc_I4_0)
.ThrowIfNotMatch("Couldn't transpile `Turret.ShootProjectile` for `Turret.unlimitedAmmo` config!")
.Set(OpCodes.Ldc_I4, int.MaxValue)
.InstructionEnumeration();
}

var il = instructions.ToList();
int maxAmmoInstructionIndex = -1;
int projectileVelocityInstructionIndex = -1;
int projectileAccuracyInstructionIndex = -1;
for (int i = 0; i < il.Count; ++i)
{
if (unlimitedAmmoEnabled &&
i + 2 < il.Count &&
il[i].LoadsField(Field_Turret_M_MaxAmmo) &&
il[i + 1].opcode == OpCodes.Ldc_I4_0 &&
il[i + 2].Branches(out _))
{
// instead of if (maxAmmo > 0) then decrement ammo, we change the 0 to max int value so that the
// condition is never satisfied.
il[i + 1] = new CodeInstruction(OpCodes.Ldc_I4, int.MaxValue);
maxAmmoInstructionIndex = i + 1;
}
private static void Postfix(Turret __instance)
{
var player = TurretHelpers.GetPlayerCreator(__instance);
if (player == null) return;

if (projectileVelocityEnabled && il[i].LoadsField(Field_Attack_M_ProjectileVel))
{
// apply Turret.projectileVelocity when `Attack.m_projectileVel` is loaded
var multiplier = Helper.applyModifierValue(1f, config.projectileVelocity);
il.InsertRange(i + 1, new CodeInstruction[]
{
new(OpCodes.Ldc_R4, multiplier),
new(OpCodes.Mul)
}
);
projectileVelocityInstructionIndex = i;
}
var projectile = __instance.m_lastProjectile?.GetComponent<Projectile>();
if (projectile == null) return;

if (projectileAccuracyEnabled && il[i].LoadsField(Field_Attack_M_ProjectileAccuracy))
{
// apply Turret.projectileVelocity when `Attack.m_projectileAccuracy` is loaded
// we invert projectileVelocity so that bigger number is better accuracy.
var multiplier = Helper.applyModifierValue(1f, -config.projectileAccuracy);
il.InsertRange(i + 1, new CodeInstruction[]
{
new(OpCodes.Ldc_R4, multiplier),
new(OpCodes.Mul)
}
);
projectileAccuracyInstructionIndex = i;
}
}
// By giving ownership of the projectile we force it to perform all necessary pvp checks.
projectile.m_owner = TurretHelpers.GetPlayerCreator(__instance);
projectile.m_raiseSkillAmount = 0;
}
}

if (unlimitedAmmoEnabled && maxAmmoInstructionIndex == -1)
ValheimPlusPlugin.Logger.LogError(
"Couldn't transpile `Turret.ShootProjectile` for `Turret.unlimitedAmmo` config!");
[HarmonyPatch(typeof(Turret), nameof(Turret.UpdateTarget))]
public static class Turret_UpdateTarget_Patch
{
private static void Postfix(Turret __instance)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a prefix?

{
var config = Configuration.Current.Turret;
if (!config.IsEnabled) return;

if (projectileVelocityEnabled && projectileVelocityInstructionIndex == -1)
ValheimPlusPlugin.Logger.LogError(
"Couldn't transpile `Turret.ShootProjectile` for `Turret.projectileVelocity` config!");
__instance.m_targetPlayers = !config.disablePvP && TurretHelpers.IsCreatorPvP(__instance);
}
}

if (projectileAccuracyEnabled && projectileAccuracyInstructionIndex == -1)
ValheimPlusPlugin.Logger.LogError(
"Couldn't transpile `Turret.ShootProjectile` for `Turret.projectileAccuracy` config!");
[HarmonyPatch(typeof(Turret), nameof(Turret.GetAmmoItem))]
public static class Turret_GetAmmoItem_Patch
{
private static void Postfix(Turret __instance, ref ItemDrop.ItemData __result)
{
if (!Configuration.Current.Turret.IsEnabled || __result == null) return;

return il.AsEnumerable();
// Under normal conditions the projectile velocity is set to the view distance.
// We maintain the game's normal accuracy coefficient by resetting the projectile velocity to the view distance.
// Patched here to maintain consistency in both UpdateTurretRotation and ShootProjectile
__result.m_shared.m_attack.m_projectileVel = __instance.m_viewDistance;
}
}
}
2 changes: 2 additions & 0 deletions ValheimPlus/ValheimPlus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
<Compile Include="Deconstruct.cs" />
<Compile Include="FirstPerson\VPlusFirstPerson.cs" />
<Compile Include="FreePlacementRotation.cs" />
<Compile Include="GameClasses\BaseAI.cs" />
<Compile Include="GameClasses\Bed.cs" />
<Compile Include="GameClasses\Beehive.cs" />
<Compile Include="Configurations\BaseConfig.cs" />
Expand Down Expand Up @@ -215,6 +216,7 @@
<Compile Include="GameClasses\LuredWisp.cs" />
<Compile Include="GameClasses\PickableItem.cs" />
<Compile Include="GameClasses\PlayerProfile.cs" />
<Compile Include="GameClasses\Projectile.cs" />
<Compile Include="GameClasses\Recipe.cs" />
<Compile Include="GameClasses\SapCollector.cs" />
<Compile Include="GameClasses\SEMan.cs" />
Expand Down
39 changes: 27 additions & 12 deletions valheim_plus.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -1291,26 +1291,41 @@ autoRange = 10
; Change false to true to enable this section.
enabled = false

; Change false to true to make the balista ignore players.
ignorePlayers = false
; By default turrets will only attack other players when a player has enabled friendly fire.
; Changing this value to true will completely disable this behavior, and turrets will never attack players.
disablePvP = false

; Change false to true to prevent consumption of Balista ammo.
unlimitedAmmo = false

; This value determines the rate at which the balista turns. A multiplier of -50 will result in the balista turning 50% faster.
turnRate = 0
; This is turn rate of the ballista in degrees per second.
; The default value is 22.5
turnRate = 22.5

; This value determines the rate of fire of the balista. A multiplier of -50 will result in the balista shooting 100% faster.
attackCooldown = 0
; This value determines the the minimum amount of time before a ballista can shoot again in seconds.
; The default value is 2.
attackCooldown = 2

; This value determines the distance a balista can see targets. A multiplier of 50 will result in the balista seeing 50% further.
viewDistance = 0
; This value determines the distance a balista can see targets in meters.
; Increasing this will allow the balista to see further, but may result in less accuracy.
; The default value is 45.
viewDistance = 45

; This value determines the velocity of the projectiles a ballista fires. A multiplier of 50 will result in the projectile velocity being 50% faster.
projectileVelocity = 0
; Change to true to enable the turret to target tamed creatures.
targetTamed = false

; This value determines the accuracy of the projectiles a ballista fires. A multiplier of 50 will result in the projectile being 50% more accurate.
projectileAccuracy = 0
; This is maximum angle that a balista can deviate to each side.
; A value of 180 is the maximum and will allow the balista to shoot in any direction.
; The default value is 50.
horizontalAngle = 50

; This is maximum angle that a balista can deviate up or down.
; A value of 90 is the maximum and will allow the balista to shoot in any direction.
; The default value is 50.
verticalAngle = 50

; Set this to true to prevent the turret from accidentally hitting itself.
fixProjectiles = false
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the name precise, turretCanNotHitItself.



[AutoStack]
Expand Down
Loading