(57c46943d) Quick fix watchman graphics (not quite right yet, but better). Re-enable the variant 2.
This commit is contained in:
@@ -100,6 +100,25 @@ namespace Barotrauma
|
||||
steeringManager = outsideSteering;
|
||||
}
|
||||
|
||||
float maxDistanceToSub = 3000;
|
||||
if (Character.Submarine != null || SelectedAiTarget?.Entity?.Submarine != null &&
|
||||
Vector2.DistanceSquared(Character.WorldPosition, SelectedAiTarget.Entity.Submarine.WorldPosition) < maxDistanceToSub * maxDistanceToSub)
|
||||
{
|
||||
if (steeringManager != insideSteering)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
}
|
||||
steeringManager = insideSteering;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (steeringManager != outsideSteering)
|
||||
{
|
||||
outsideSteering.Reset();
|
||||
}
|
||||
steeringManager = outsideSteering;
|
||||
}
|
||||
|
||||
AnimController.Crouching = shouldCrouch;
|
||||
CheckCrouching(deltaTime);
|
||||
Character.ClearInputs();
|
||||
@@ -483,7 +502,11 @@ namespace Barotrauma
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
|
||||
//TODO: re-enable on all languages after DialogNoRescueTargets has been translated
|
||||
if (TextManager.Language == "English")
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
|
||||
}
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
|
||||
{
|
||||
@@ -534,6 +557,8 @@ namespace Barotrauma
|
||||
|
||||
public static bool HasItem(Character character, string tag, string containedTag, float conditionPercentage = 0)
|
||||
{
|
||||
if (character == null) { return false; }
|
||||
if (character.Inventory == null) { return false; }
|
||||
var item = character.Inventory.FindItemByTag(tag);
|
||||
return item != null &&
|
||||
item.ConditionPercentage > conditionPercentage &&
|
||||
|
||||
@@ -164,7 +164,15 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var newPath = pathFinder.FindPath(pos, target, "(Character: " + character.Name + ")");
|
||||
if (currentPath == null || needsNewPath || !newPath.Unreachable && newPath.Cost < currentPath.Cost)
|
||||
bool useNewPath = currentPath == null || needsNewPath;
|
||||
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
|
||||
{
|
||||
// It's possible that the current path was calculated from a start point that is no longer valid.
|
||||
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
|
||||
useNewPath = newPath.Cost < currentPath.Cost ||
|
||||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 2, 2);
|
||||
}
|
||||
if (useNewPath)
|
||||
{
|
||||
currentPath = newPath;
|
||||
}
|
||||
@@ -406,7 +414,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (door.HasIntegratedButtons)
|
||||
{
|
||||
door.Item.TryInteract(character, false, true, true);
|
||||
door.Item.TryInteract(character, false, true);
|
||||
buttonPressCooldown = ButtonPressInterval;
|
||||
break;
|
||||
}
|
||||
@@ -414,7 +422,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2))
|
||||
{
|
||||
closestButton.Item.TryInteract(character, false, true, false);
|
||||
closestButton.Item.TryInteract(character, false, true);
|
||||
buttonPressCooldown = ButtonPressInterval;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace Barotrauma
|
||||
if (seekAmmunition == null || !subObjectives.Contains(seekAmmunition))
|
||||
{
|
||||
Move();
|
||||
if (Weapon != null)
|
||||
if (WeaponComponent != null)
|
||||
{
|
||||
OperateWeapon(deltaTime);
|
||||
}
|
||||
@@ -279,24 +279,6 @@ namespace Barotrauma
|
||||
Weapon.Drop(character);
|
||||
}
|
||||
}
|
||||
//if (!character.SelectedItems.Contains(Weapon))
|
||||
if (!character.HasEquippedItem(Weapon))
|
||||
{
|
||||
Weapon.TryInteract(character, forceSelectKey: true);
|
||||
var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
|
||||
if (character.Inventory.TryPutItem(Weapon, character, slots))
|
||||
{
|
||||
Weapon.Equip(character);
|
||||
aimTimer = Rand.Range(0.5f, 1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Weapon = null;
|
||||
Mode = CombatMode.Retreat;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool Equip()
|
||||
@@ -338,7 +320,10 @@ namespace Barotrauma
|
||||
{
|
||||
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
|
||||
}
|
||||
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true));
|
||||
if (character.CurrentHull != retreatTarget)
|
||||
{
|
||||
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true));
|
||||
}
|
||||
}
|
||||
|
||||
private void Engage()
|
||||
@@ -354,8 +339,7 @@ namespace Barotrauma
|
||||
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
|
||||
{
|
||||
AllowGoingOutside = true,
|
||||
IgnoreIfTargetDead = true,
|
||||
CheckVisibility = true
|
||||
IgnoreIfTargetDead = true
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -365,7 +349,7 @@ namespace Barotrauma
|
||||
if (followTargetObjective != null && subObjectives.Contains(followTargetObjective))
|
||||
{
|
||||
followTargetObjective.CloseEnough =
|
||||
WeaponComponent is RangedWeapon ? 3 :
|
||||
WeaponComponent is RangedWeapon ? 300 :
|
||||
WeaponComponent is MeleeWeapon mw ? mw.Range :
|
||||
WeaponComponent is RepairTool rt ? rt.Range : 50;
|
||||
}
|
||||
@@ -455,7 +439,7 @@ namespace Barotrauma
|
||||
float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position);
|
||||
character.CursorPosition = Enemy.Position;
|
||||
float engageDistance = 500;
|
||||
if (squaredDistance > engageDistance * engageDistance) { return; }
|
||||
if (character.CurrentHull != Enemy.CurrentHull && squaredDistance > engageDistance * engageDistance) { return; }
|
||||
if (!character.CanSeeCharacter(Enemy)) { return; }
|
||||
if (Weapon.RequireAimToUse)
|
||||
{
|
||||
@@ -468,7 +452,7 @@ namespace Barotrauma
|
||||
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
|
||||
}
|
||||
}
|
||||
if (!isOperatingButtons && character.SelectedConstruction == null)
|
||||
if (!isOperatingButtons)
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -17,17 +16,21 @@ namespace Barotrauma
|
||||
|
||||
public Func<bool> customCondition;
|
||||
|
||||
public bool followControlledCharacter;
|
||||
public bool mimic;
|
||||
|
||||
/// <summary>
|
||||
/// Display units
|
||||
/// </summary>
|
||||
public float CloseEnough { get; set; } = 50;
|
||||
public bool IgnoreIfTargetDead { get; set; }
|
||||
public bool AllowGoingOutside { get; set; }
|
||||
public bool CheckVisibility { get; set; }
|
||||
|
||||
public ISpatialEntity Target { get; private set; }
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (FollowControlledCharacter && Character.Controlled == null) { return 0.0f; }
|
||||
if (followControlledCharacter && Character.Controlled == null) { return 0.0f; }
|
||||
if (Target is Entity e && e.Removed) { return 0.0f; }
|
||||
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) { return 0.0f; }
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
@@ -37,23 +40,19 @@ namespace Barotrauma
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
public ISpatialEntity Target { get; private set; }
|
||||
|
||||
public bool FollowControlledCharacter;
|
||||
|
||||
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1)
|
||||
: base (character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.Target = target;
|
||||
this.repeat = repeat;
|
||||
waitUntilPathUnreachable = 2.0f;
|
||||
waitUntilPathUnreachable = 3.0f;
|
||||
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
|
||||
CalculateCloseEnough();
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (FollowControlledCharacter)
|
||||
if (followControlledCharacter)
|
||||
{
|
||||
if (Character.Controlled == null)
|
||||
{
|
||||
@@ -92,11 +91,18 @@ namespace Barotrauma
|
||||
{
|
||||
abandon = true;
|
||||
}
|
||||
else if (!repeat && waitUntilPathUnreachable < 0)
|
||||
else if (waitUntilPathUnreachable < 0)
|
||||
{
|
||||
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null)
|
||||
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
abandon = PathSteering.CurrentPath.Unreachable;
|
||||
if (repeat)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (abandon)
|
||||
@@ -115,9 +121,9 @@ namespace Barotrauma
|
||||
Vector2 currTargetSimPos = Vector2.Zero;
|
||||
currTargetSimPos = Target.SimPosition;
|
||||
// Take the sub position into account in the sim pos
|
||||
if (character.Submarine == null && Target.Submarine != null)
|
||||
if (SteeringManager != PathSteering && character.Submarine == null && Target.Submarine != null)
|
||||
{
|
||||
//currTargetSimPos += Target.Submarine.SimPosition;
|
||||
currTargetSimPos += Target.Submarine.SimPosition;
|
||||
}
|
||||
else if (character.Submarine != null && Target.Submarine == null)
|
||||
{
|
||||
@@ -132,10 +138,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
character.AIController.SteeringManager.SteeringSeek(currTargetSimPos);
|
||||
if (SteeringManager != PathSteering)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1, heading: VectorExtensions.Forward(character.AnimController.Collider.Rotation));
|
||||
}
|
||||
if (getDivingGearIfNeeded)
|
||||
{
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(targetHull);
|
||||
bool needsDivingSuit = needsDivingGear && (targetHull == null || targetIsOutside || targetHull.WaterPercentage > 90);
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(targetHull) || mimic && HumanAIController.HasDivingMask(followTarget);
|
||||
bool needsDivingSuit = needsDivingGear && (targetHull == null || targetIsOutside || targetHull.WaterPercentage > 90) || mimic && HumanAIController.HasDivingSuit(followTarget);
|
||||
bool needsEquipment = false;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
|
||||
@@ -239,7 +239,8 @@ namespace Barotrauma
|
||||
CloseEnough = 150,
|
||||
AllowGoingOutside = true,
|
||||
IgnoreIfTargetDead = true,
|
||||
FollowControlledCharacter = orderGiver == character
|
||||
followControlledCharacter = orderGiver == character,
|
||||
mimic = true
|
||||
};
|
||||
break;
|
||||
case "wait":
|
||||
@@ -258,7 +259,7 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
|
||||
break;
|
||||
case "repairsystems":
|
||||
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) { RequireAdequateSkills = option != "all" };
|
||||
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) { RequireAdequateSkills = option == "jobspecific" };
|
||||
break;
|
||||
case "pumpwater":
|
||||
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
|
||||
|
||||
@@ -68,6 +68,12 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
|
||||
{
|
||||
// Don't allow to operate an item that someone already operates, unless this objective is an order
|
||||
if (objectiveManager.CurrentOrder != this && Character.CharacterList.Any(c => c.SelectedConstruction == target.Item && c != character && HumanAIController.IsFriendly(c)))
|
||||
{
|
||||
abandon = true;
|
||||
return;
|
||||
}
|
||||
if (character.SelectedConstruction != target.Item)
|
||||
{
|
||||
target.Item.TryInteract(character, false, true);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -17,13 +16,22 @@ namespace Barotrauma
|
||||
private readonly Character targetCharacter;
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
|
||||
private float treatmentTimer;
|
||||
private Hull safeHull;
|
||||
|
||||
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
if (targetCharacter != character)
|
||||
if (targetCharacter == null)
|
||||
{
|
||||
string errorMsg = "Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
abandon = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetCharacter == character)
|
||||
{
|
||||
// TODO: enable healing self too
|
||||
abandon = true;
|
||||
@@ -40,6 +48,11 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (targetCharacter == null || targetCharacter.Removed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Unconcious target is not in a safe place -> Move to a safe place first
|
||||
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
@@ -71,15 +84,22 @@ namespace Barotrauma
|
||||
{
|
||||
goToObjective = null;
|
||||
}
|
||||
var findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
|
||||
if (findSafety == null)
|
||||
if (safeHull == null)
|
||||
{
|
||||
// Ensure that we have the find safety objective (should always be the case)
|
||||
objectiveManager.AddObjective(new AIObjectiveFindSafety(character, objectiveManager));
|
||||
var findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
|
||||
if (findSafety == null)
|
||||
{
|
||||
// Ensure that we have the find safety objective (should always be the case)
|
||||
findSafety = new AIObjectiveFindSafety(character, objectiveManager);
|
||||
objectiveManager.AddObjective(findSafety);
|
||||
}
|
||||
safeHull = findSafety.FindBestHull(HumanAIController.VisibleHulls);
|
||||
}
|
||||
if (character.CurrentHull != safeHull)
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager));
|
||||
}
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(findSafety.FindBestHull(HumanAIController.VisibleHulls), character, objectiveManager));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subObjectives.Any()) { return; }
|
||||
@@ -207,13 +227,19 @@ namespace Barotrauma
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
if (targetCharacter == null || targetCharacter.Removed)
|
||||
{
|
||||
abandon = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isCompleted = targetCharacter.Bleeding <= 0 && targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager);
|
||||
if (isCompleted)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogTargetHealed").Replace("[targetname]", targetCharacter.Name),
|
||||
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
return isCompleted || targetCharacter.Removed || targetCharacter.IsDead;
|
||||
return isCompleted || targetCharacter.IsDead;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
|
||||
@@ -45,9 +45,9 @@ namespace Barotrauma
|
||||
steering += DoSteeringWander(weight);
|
||||
}
|
||||
|
||||
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1)
|
||||
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1, Vector2? heading = null)
|
||||
{
|
||||
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight);
|
||||
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight, heading);
|
||||
}
|
||||
|
||||
public void SteeringManual(float deltaTime, Vector2 velocity)
|
||||
@@ -129,10 +129,12 @@ namespace Barotrauma
|
||||
return newSteering;
|
||||
}
|
||||
|
||||
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float weight)
|
||||
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float weight, Vector2? heading = null)
|
||||
{
|
||||
if (steering == Vector2.Zero || host.Steering == Vector2.Zero) return Vector2.Zero;
|
||||
|
||||
if (steering == Vector2.Zero || host.Steering == Vector2.Zero)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
float maxDistance = lookAheadDistance;
|
||||
if (rayCastTimer <= 0.0f)
|
||||
{
|
||||
@@ -158,19 +160,11 @@ namespace Barotrauma
|
||||
{
|
||||
obstaclePosition.X = closestStructure.SimPosition.X;
|
||||
}
|
||||
|
||||
avoidObstaclePos = obstaclePosition;
|
||||
//avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition);
|
||||
}
|
||||
/*else if (closestBody.UserData is Item)
|
||||
{
|
||||
avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - item.SimPosition);
|
||||
}*/
|
||||
else
|
||||
{
|
||||
|
||||
avoidObstaclePos = Submarine.LastPickedPosition;
|
||||
//avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,14 +179,26 @@ namespace Barotrauma
|
||||
Vector2 diff = avoidObstaclePos.Value - host.SimPosition;
|
||||
float dist = diff.Length();
|
||||
|
||||
if (!avoidObstaclePos.HasValue) return Vector2.Zero;
|
||||
|
||||
Vector2 diff = avoidObstaclePos.Value - host.SimPosition;
|
||||
float dist = diff.Length();
|
||||
|
||||
if (dist > maxDistance) return Vector2.Zero;
|
||||
|
||||
return -diff * (1.0f - dist / maxDistance) * weight;
|
||||
if (dist > maxDistance)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
if (heading.HasValue)
|
||||
{
|
||||
var f = heading ?? host.Steering;
|
||||
// Avoid to left or right depending on the current heading
|
||||
Vector2 relativeVector = Vector2.Normalize(diff) - Vector2.Normalize(f);
|
||||
var dir = relativeVector.X > 0 ? diff.Right() : diff.Left();
|
||||
float factor = 1.0f - Math.Min(dist / maxDistance, 1);
|
||||
return dir * factor * weight;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Doesn't work right because it effectively just slows down or reverses the movement, where as we'd like to go right or left to avoid the target.
|
||||
// There's also another issue, which also affects going right or left: the raycast doesn't hit anything if we turn too much -> avoiding doesn't work well.
|
||||
// Could probably "remember" the avoidance a bit longer so that the avoid steering is not immedieately disgarded, but kept for a while and reduced gradually?
|
||||
return -diff * (1.0f - dist / maxDistance) * weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,11 @@ namespace Barotrauma
|
||||
errorMessages = new List<string>();
|
||||
foreach (ContentFile file in Files)
|
||||
{
|
||||
#if SERVER
|
||||
//dedicated server doesn't care if the client executable is present or not
|
||||
if (file.Type == ContentType.Executable) { continue; }
|
||||
#endif
|
||||
|
||||
if (!File.Exists(file.Path))
|
||||
{
|
||||
errorMessages.Add("File \"" + file.Path + "\" not found.");
|
||||
|
||||
@@ -204,6 +204,7 @@ namespace Barotrauma
|
||||
{
|
||||
timer = time;
|
||||
TotalTime = time;
|
||||
this.ignorePause = ignorePause;
|
||||
}
|
||||
|
||||
public bool CheckFinished(float deltaTime)
|
||||
|
||||
@@ -83,11 +83,15 @@ namespace Barotrauma
|
||||
Commonness = element.GetAttributeInt("commonness", 1);
|
||||
|
||||
SuccessMessage = TextManager.Get("MissionSuccess." + Identifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully");
|
||||
FailureMessage =
|
||||
TextManager.Get("MissionFailure." + Identifier, true) ??
|
||||
element.GetAttributeString("failuremessage", null) ??
|
||||
TextManager.Get("missionfailed", returnNull: true) ??
|
||||
"";
|
||||
FailureMessage = TextManager.Get("MissionFailure." + Identifier, true) ?? "";
|
||||
if (string.IsNullOrEmpty(FailureMessage) && TextManager.ContainsTag("missionfailed"))
|
||||
{
|
||||
FailureMessage = TextManager.Get("missionfailed", returnNull: true) ?? "";
|
||||
}
|
||||
if (string.IsNullOrEmpty(FailureMessage) && GameMain.Config.Language == "English")
|
||||
{
|
||||
FailureMessage = element.GetAttributeString("failuremessage", "");
|
||||
}
|
||||
|
||||
SonarLabel = TextManager.Get("MissionSonarLabel." + Identifier, true) ?? element.GetAttributeString("sonarlabel", "");
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ namespace Barotrauma.Extensions
|
||||
/// </summary>
|
||||
public static Vector2 TransformVector(this Vector2 v, Vector2 up)
|
||||
{
|
||||
up = Vector2.Normalize(up);
|
||||
return (up * v.Y) + (up.Right() * v.X);
|
||||
}
|
||||
|
||||
|
||||
@@ -337,6 +337,79 @@ namespace Barotrauma
|
||||
keyMapping[(int)InputType.SelectNextCharacter] = new KeyOrMouse(Keys.Z);
|
||||
keyMapping[(int)InputType.SelectPreviousCharacter] = new KeyOrMouse(Keys.X);
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckBindings(bool useDefaults)
|
||||
{
|
||||
foreach (InputType inputType in Enum.GetValues(typeof(InputType)))
|
||||
{
|
||||
var binding = keyMapping[(int)inputType];
|
||||
if (binding == null)
|
||||
{
|
||||
switch (inputType)
|
||||
{
|
||||
case InputType.Deselect:
|
||||
if (useDefaults)
|
||||
{
|
||||
binding = new KeyOrMouse(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy support
|
||||
var selectKey = keyMapping[(int)InputType.Select];
|
||||
if (selectKey != null && selectKey.Key != Keys.None)
|
||||
{
|
||||
binding = new KeyOrMouse(selectKey.Key);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case InputType.Shoot:
|
||||
if (useDefaults)
|
||||
{
|
||||
binding = new KeyOrMouse(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy support
|
||||
var useKey = keyMapping[(int)InputType.Use];
|
||||
if (useKey != null && useKey.MouseButton.HasValue)
|
||||
{
|
||||
binding = new KeyOrMouse(useKey.MouseButton.Value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (binding == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Key binding for the input type \"" + inputType + " not set!");
|
||||
binding = new KeyOrMouse(Keys.D1);
|
||||
}
|
||||
keyMapping[(int)inputType] = binding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Load DefaultConfig
|
||||
private void LoadDefaultConfig(bool setLanguage = true)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(savePath);
|
||||
|
||||
if (setLanguage || string.IsNullOrEmpty(Language))
|
||||
{
|
||||
Language = doc.Root.GetAttributeString("language", "English");
|
||||
}
|
||||
|
||||
MasterServerUrl = doc.Root.GetAttributeString("masterserverurl", "");
|
||||
|
||||
AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", true);
|
||||
WasGameUpdated = doc.Root.GetAttributeBool("wasgameupdated", false);
|
||||
|
||||
VerboseLogging = doc.Root.GetAttributeBool("verboselogging", false);
|
||||
SaveDebugConsoleLogs = doc.Root.GetAttributeBool("savedebugconsolelogs", false);
|
||||
|
||||
QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", "");
|
||||
|
||||
if (legacy)
|
||||
{
|
||||
@@ -605,6 +678,54 @@ namespace Barotrauma
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Save DefaultConfig
|
||||
private void SaveNewDefaultConfig()
|
||||
{
|
||||
XDocument doc = new XDocument();
|
||||
|
||||
if (doc.Root == null)
|
||||
{
|
||||
doc.Add(new XElement("config"));
|
||||
}
|
||||
|
||||
doc.Root.Add(
|
||||
new XAttribute("language", TextManager.Language),
|
||||
new XAttribute("masterserverurl", MasterServerUrl),
|
||||
new XAttribute("autocheckupdates", AutoCheckUpdates),
|
||||
new XAttribute("musicvolume", musicVolume),
|
||||
new XAttribute("soundvolume", soundVolume),
|
||||
new XAttribute("voicechatvolume", voiceChatVolume),
|
||||
new XAttribute("verboselogging", VerboseLogging),
|
||||
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
new XAttribute("usesteammatchmaking", useSteamMatchmaking),
|
||||
new XAttribute("quickstartsub", QuickStartSubmarineName),
|
||||
new XAttribute("requiresteamauthentication", requireSteamAuthentication),
|
||||
new XAttribute("aimassistamount", aimAssistAmount));
|
||||
|
||||
if (!ShowUserStatisticsPrompt)
|
||||
{
|
||||
doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics));
|
||||
}
|
||||
|
||||
if (WasGameUpdated)
|
||||
{
|
||||
doc.Root.Add(new XAttribute("wasgameupdated", true));
|
||||
}
|
||||
if (!SelectedContentPackages.Any())
|
||||
{
|
||||
var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage);
|
||||
if (availablePackage != null)
|
||||
{
|
||||
SelectedContentPackages.Add(availablePackage);
|
||||
}
|
||||
}
|
||||
|
||||
//save to get rid of the invalid selected packages in the config file
|
||||
if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0) { SaveNewPlayerConfig(); }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Save DefaultConfig
|
||||
private void SaveNewDefaultConfig()
|
||||
{
|
||||
@@ -750,6 +871,7 @@ namespace Barotrauma
|
||||
if (!fileFound)
|
||||
{
|
||||
ShowLanguageSelectionPrompt = true;
|
||||
ShowUserStatisticsPrompt = true;
|
||||
SaveNewPlayerConfig();
|
||||
}
|
||||
}
|
||||
@@ -820,58 +942,6 @@ namespace Barotrauma
|
||||
{
|
||||
VoiceSetting = voiceSetting;
|
||||
}
|
||||
foreach (ContentFile file in contentPackage.Files)
|
||||
{
|
||||
ToolBox.IsProperFilenameCase(file.Path);
|
||||
}
|
||||
}
|
||||
if (!SelectedContentPackages.Any())
|
||||
{
|
||||
var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage);
|
||||
if (availablePackage != null)
|
||||
{
|
||||
SelectedContentPackages.Add(availablePackage);
|
||||
}
|
||||
}
|
||||
|
||||
//save to get rid of the invalid selected packages in the config file
|
||||
if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0) { SaveNewPlayerConfig(); }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Save DefaultConfig
|
||||
private void SaveNewDefaultConfig()
|
||||
{
|
||||
XDocument doc = new XDocument();
|
||||
|
||||
if (doc.Root == null)
|
||||
{
|
||||
doc.Add(new XElement("config"));
|
||||
}
|
||||
|
||||
doc.Root.Add(
|
||||
new XAttribute("language", TextManager.Language),
|
||||
new XAttribute("masterserverurl", MasterServerUrl),
|
||||
new XAttribute("autocheckupdates", AutoCheckUpdates),
|
||||
new XAttribute("musicvolume", musicVolume),
|
||||
new XAttribute("soundvolume", soundVolume),
|
||||
new XAttribute("voicechatvolume", voiceChatVolume),
|
||||
new XAttribute("verboselogging", VerboseLogging),
|
||||
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
new XAttribute("usesteammatchmaking", useSteamMatchmaking),
|
||||
new XAttribute("quickstartsub", QuickStartSubmarineName),
|
||||
new XAttribute("requiresteamauthentication", requireSteamAuthentication),
|
||||
new XAttribute("aimassistamount", aimAssistAmount));
|
||||
|
||||
if (!ShowUserStatisticsPrompt)
|
||||
{
|
||||
doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics));
|
||||
}
|
||||
|
||||
if (WasGameUpdated)
|
||||
{
|
||||
doc.Root.Add(new XAttribute("wasgameupdated", true));
|
||||
}
|
||||
|
||||
useSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", useSteamMatchmaking);
|
||||
@@ -940,7 +1010,22 @@ namespace Barotrauma
|
||||
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
gSettings = new XElement("graphicssettings");
|
||||
doc.Root.Add(gSettings);
|
||||
}
|
||||
|
||||
gSettings.ReplaceAttributes(
|
||||
new XAttribute("particlelimit", ParticleLimit),
|
||||
new XAttribute("lightmapscale", LightMapScale),
|
||||
new XAttribute("specularity", SpecularityEnabled),
|
||||
new XAttribute("chromaticaberration", ChromaticAberrationEnabled),
|
||||
new XAttribute("losmode", LosMode),
|
||||
new XAttribute("hudscale", HUDScale),
|
||||
new XAttribute("inventoryscale", InventoryScale));
|
||||
|
||||
foreach (ContentPackage contentPackage in SelectedContentPackages)
|
||||
{
|
||||
if (contentPackage.Path.Contains(vanillaContentPackagePath))
|
||||
{
|
||||
case "contentpackage":
|
||||
string path = System.IO.Path.GetFullPath(subElement.GetAttributeString("path", ""));
|
||||
@@ -1054,6 +1139,7 @@ namespace Barotrauma
|
||||
new XAttribute("autocheckupdates", AutoCheckUpdates),
|
||||
new XAttribute("musicvolume", musicVolume),
|
||||
new XAttribute("soundvolume", soundVolume),
|
||||
new XAttribute("voicechatvolume", voiceChatVolume),
|
||||
new XAttribute("verboselogging", VerboseLogging),
|
||||
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
|
||||
@@ -79,14 +79,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
wires = new Wire[MaxLinked];
|
||||
|
||||
if (string.IsNullOrEmpty(DisplayName))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name);
|
||||
#endif
|
||||
DisplayName = Name;
|
||||
}
|
||||
IsOutput = element.Name.ToString() == "output";
|
||||
Name = element.GetAttributeString("name", IsOutput ? "output" : "input");
|
||||
|
||||
string displayNameTag = "", fallbackTag = "";
|
||||
//if displayname is not present, attempt to find it from the prefab
|
||||
if (element.Attribute("displayname") == null)
|
||||
{
|
||||
@@ -98,26 +94,44 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (connectionElement.Name.ToString() != element.Name.ToString()) { continue; }
|
||||
|
||||
string prefabConnectionName = element.GetAttributeString("name", (IsOutput) ? "output" : "input");
|
||||
string prefabConnectionName = element.GetAttributeString("name", IsOutput ? "output" : "input");
|
||||
if (prefabConnectionName == Name)
|
||||
{
|
||||
DisplayName = TextManager.GetServerMessage(connectionElement.GetAttributeString("displayname", Name));
|
||||
displayNameTag = connectionElement.GetAttributeString("displayname", "");
|
||||
fallbackTag = connectionElement.GetAttributeString("fallbackdisplayname", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
if (string.IsNullOrEmpty(DisplayName))
|
||||
{
|
||||
DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayName = TextManager.GetServerMessage(element.GetAttributeString("displayname", Name));
|
||||
displayNameTag = element.GetAttributeString("displayname", "");
|
||||
fallbackTag = element.GetAttributeString("fallbackdisplayname", null);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(displayNameTag))
|
||||
{
|
||||
//extract the tag parts in case the tags contains variables
|
||||
string tagWithoutVariables = displayNameTag?.Split('~')?.FirstOrDefault();
|
||||
string fallbackTagWithoutVariables = fallbackTag?.Split('~')?.FirstOrDefault();
|
||||
//use displayNameTag if found, otherwise fallBack
|
||||
if (TextManager.ContainsTag(tagWithoutVariables))
|
||||
{
|
||||
DisplayName = TextManager.GetServerMessage(displayNameTag);
|
||||
}
|
||||
else if (TextManager.ContainsTag(fallbackTagWithoutVariables))
|
||||
{
|
||||
DisplayName = TextManager.GetServerMessage(fallbackTag);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(DisplayName))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name);
|
||||
#endif
|
||||
DisplayName = Name;
|
||||
}
|
||||
|
||||
IsPower = Name == "power_in" || Name == "power" || Name == "power_out";
|
||||
|
||||
|
||||
@@ -1641,7 +1641,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!ic.HasRequiredContainedItems(user == Character.Controlled)) continue;
|
||||
|
||||
bool success = Rand.Range(0.0f, 1.0f) < ic.DegreeOfSuccess(user);
|
||||
bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user);
|
||||
ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure;
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -119,23 +119,40 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.GameSession != null && Character.Controlled != null)
|
||||
if (GameMain.GameSession != null)
|
||||
{
|
||||
if (Character.Controlled.HasEquippedItem("clownmask") &&
|
||||
Character.Controlled.HasEquippedItem("clowncostume"))
|
||||
#if CLIENT
|
||||
if (Character.Controlled != null) { CheckMidRoundAchievements(Character.Controlled); }
|
||||
#else
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
UnlockAchievement(Character.Controlled, "clowncostume");
|
||||
}
|
||||
|
||||
if (Submarine.MainSub != null && Character.Controlled.Submarine == null)
|
||||
{
|
||||
float dist = 500 / Physics.DisplayToRealWorldRatio;
|
||||
if (Vector2.DistanceSquared(Character.Controlled.WorldPosition, Submarine.MainSub.WorldPosition) >
|
||||
dist * dist)
|
||||
if (client.Character != null)
|
||||
{
|
||||
UnlockAchievement(Character.Controlled, "crewaway");
|
||||
CheckMidRoundAchievements(client.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckMidRoundAchievements(Character c)
|
||||
{
|
||||
if (c == null || c.Removed) { return; }
|
||||
|
||||
if (c.HasEquippedItem("clownmask") &&
|
||||
c.HasEquippedItem("clowncostume"))
|
||||
{
|
||||
UnlockAchievement(c, "clowncostume");
|
||||
}
|
||||
|
||||
if (Submarine.MainSub != null && c.Submarine == null)
|
||||
{
|
||||
float dist = 500 / Physics.DisplayToRealWorldRatio;
|
||||
if (Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition) >
|
||||
dist * dist)
|
||||
{
|
||||
UnlockAchievement(c, "crewaway");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +231,8 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (character.HasEquippedItem("clownmask") &&
|
||||
character.HasEquippedItem("clowncostume"))
|
||||
character.HasEquippedItem("clowncostume") &&
|
||||
causeOfDeath.Killer != character)
|
||||
{
|
||||
UnlockAchievement(causeOfDeath.Killer, "killclown");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -42,6 +43,26 @@ namespace Barotrauma
|
||||
GetTextFilesRecursive(subDir, ref list);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of the language in the respective language
|
||||
/// </summary>
|
||||
public static string GetTranslatedLanguageName(string language)
|
||||
{
|
||||
if (!textPacks.ContainsKey(language))
|
||||
{
|
||||
return language;
|
||||
}
|
||||
|
||||
foreach (var textPack in textPacks[language])
|
||||
{
|
||||
if (textPack.Language == language)
|
||||
{
|
||||
return textPack.TranslatedName;
|
||||
}
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
public static void LoadTextPacks(IEnumerable<ContentPackage> selectedContentPackages)
|
||||
{
|
||||
@@ -80,6 +101,26 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ContainsTag(string textTag)
|
||||
{
|
||||
if (string.IsNullOrEmpty(textTag)) { return false; }
|
||||
|
||||
if (!textPacks.ContainsKey(Language))
|
||||
{
|
||||
DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English...");
|
||||
Language = "English";
|
||||
if (!textPacks.ContainsKey(Language))
|
||||
{
|
||||
throw new Exception("No text packs available in English!");
|
||||
}
|
||||
}
|
||||
foreach (TextPack textPack in textPacks[Language])
|
||||
{
|
||||
if (textPack.Get(textTag) != null) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string Get(string textTag, bool returnNull = false, string fallBackTag = null)
|
||||
{
|
||||
if (!textPacks.ContainsKey(Language))
|
||||
@@ -91,8 +132,6 @@ namespace Barotrauma
|
||||
throw new Exception("No text packs available in English!");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (TextPack textPack in textPacks[Language])
|
||||
{
|
||||
@@ -116,7 +155,13 @@ namespace Barotrauma
|
||||
foreach (TextPack textPack in textPacks["English"])
|
||||
{
|
||||
string text = textPack.Get(textTag);
|
||||
if (text != null) return text;
|
||||
if (text != null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage("Text \"" + textTag + "\" not found for the language \"" + Language + "\". Using the English text \"" + text + "\" instead.");
|
||||
#endif
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,42 +412,6 @@ namespace Barotrauma
|
||||
return isCJK.IsMatch(text);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public static void CheckForDuplicates(string lang)
|
||||
{
|
||||
if (!textPacks.ContainsKey(lang))
|
||||
{
|
||||
DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
|
||||
return;
|
||||
}
|
||||
|
||||
int packIndex = 0;
|
||||
foreach (TextPack textPack in textPacks[lang])
|
||||
{
|
||||
textPack.CheckForDuplicates(packIndex);
|
||||
packIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteToCSV()
|
||||
{
|
||||
string lang = "English";
|
||||
|
||||
if (!textPacks.ContainsKey(lang))
|
||||
{
|
||||
DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
|
||||
return;
|
||||
}
|
||||
|
||||
int packIndex = 0;
|
||||
foreach (TextPack textPack in textPacks[lang])
|
||||
{
|
||||
textPack.WriteToCSV(packIndex);
|
||||
packIndex++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
public static void CheckForDuplicates(string lang)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,11 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly string Language;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the language in the language this pack is written in
|
||||
/// </summary>
|
||||
public readonly string TranslatedName;
|
||||
|
||||
private Dictionary<string, List<string>> texts;
|
||||
|
||||
private readonly string filePath;
|
||||
@@ -23,6 +28,7 @@ namespace Barotrauma
|
||||
if (doc == null || doc.Root == null) return;
|
||||
|
||||
Language = doc.Root.GetAttributeString("language", "Unknown");
|
||||
TranslatedName = doc.Root.GetAttributeString("translatedname", Language);
|
||||
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user