-
Notifications
You must be signed in to change notification settings - Fork 37
Turret #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Turret #121
Changes from all commits
ce5d5f3
d2c6eb3
8424bcd
e4eae6d
b608949
a860d30
9502ff9
e5c8932
989eb94
c2a32db
0b42938
e3219c5
33a931d
fd2622b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,12 +2,14 @@ | |
| { | ||
| public class TurretConfiguration : ServerSyncConfig<TurretConfiguration> | ||
| { | ||
| public bool ignorePlayers { get; internal set; } = false; | ||
| public bool disablePvP { get; internal set; } = false; | ||
| 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use |
||
| public bool fixProjectiles { get; internal set; } = false; | ||
| } | ||
| } | ||
| 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(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should |
||
| } | ||
|
|
||
| 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") | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| 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) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My IDE flags this saying that |
||
| { | ||
| var turretOwner = TurretHelpers.GetPlayerCreator(turret); | ||
| if (turretOwner == __instance.m_owner) | ||
| { | ||
| ValheimPlusPlugin.Logger.LogInfo("Turret projectile hit itself"); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make the name precise, |
||
|
|
||
|
|
||
| [AutoStack] | ||
|
|
||
There was a problem hiding this comment.
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
ignorePlayersand also add the newdisablePvPconfig?