(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -28,6 +28,8 @@
<Item file="Content/Items/Materials/medmaterials.xml" />
<Item file="Content/Items/Materials/minerals.xml" />
<Item file="Content/Items/Medical/medical.xml" />
<Item file="Content/Items/Medical/poisons.xml" />
<Item file="Content/Items/Medical/buffs.xml" />
<Item file="Content/Items/Misc/misc.xml" />
<Item file="Content/Items/OxygenGenerator/oxygengenerator.xml" />
<Item file="Content/Items/Pump/pump.xml" />
@@ -78,29 +80,32 @@
<Outpost file="Content/Map/Outposts/Outpost.sub" />
<Outpost file="Content/Map/Outposts/Outpost2.sub" />
<Outpost file="Content/Map/Outposts/Outpost3.sub" />
<Submarine file="Submarines/Orca.sub" />
<Submarine file="Submarines/Typhon.sub" />
<Submarine file="Submarines/Selkie.sub" />
<Submarine file="Submarines/Bunyip.sub" />
<Submarine file="Submarines/Berilia.sub" />
<Submarine file="Submarines/Dugong.sub" />
<Submarine file="Submarines/Hemulen.sub" />
<Submarine file="Submarines/Humpback.sub" />
<Submarine file="Submarines/Kastrull.sub" />
<Submarine file="Submarines/KastrullDrone.sub" />
<Submarine file="Submarines/Dugong.sub" />
<Submarine file="Submarines/Venture.sub" />
<Submarine file="Submarines/Orca.sub" />
<Submarine file="Submarines/Remora.sub" />
<Submarine file="Submarines/RemoraDrone.sub" />
<Text file="Content/Texts/EnglishVanilla.xml" />
<Text file="Content/Texts/GermanVanilla.xml" />
<Text file="Content/Texts/FrenchVanilla.xml" />
<Text file="Content/Texts/RussianVanilla.xml" />
<Text file="Content/Texts/BrazilianPortugueseVanilla.xml" />
<Text file="Content/Texts/SimplifiedChineseVanilla.xml" />
<Text file="Content/Texts/TraditionalChineseVanilla.xml" />
<Text file="Content/Texts/CastilianSpanishVanilla.xml" />
<Text file="Content/Texts/JapaneseVanilla.xml" />
<Text file="Content/Texts/LatinamericanSpanishVanilla.xml" />
<Text file="Content/Texts/PolishVanilla.xml" />
<Text file="Content/Texts/TurkishVanilla.xml" />
<Submarine file="Submarines/Selkie.sub" />
<Submarine file="Submarines/Typhon.sub" />
<Submarine file="Submarines/Typhon2.sub" />
<Submarine file="Submarines/Venture.sub" />
<Text file="Content/Texts/English/EnglishVanilla.xml" />
<Text file="Content/Texts/German/GermanVanilla.xml" />
<Text file="Content/Texts/French/FrenchVanilla.xml" />
<Text file="Content/Texts/Russian/RussianVanilla.xml" />
<Text file="Content/Texts/Polish/PolishVanilla.xml" />
<Text file="Content/Texts/BrazilianPortuguese/BrazilianPortugueseVanilla.xml" />
<Text file="Content/Texts/CastilianSpanish/CastilianSpanishVanilla.xml" />
<Text file="Content/Texts/LatinamericanSpanish/LatinamericanSpanishVanilla.xml" />
<Text file="Content/Texts/SimplifiedChinese/SimplifiedChineseVanilla.xml" />
<Text file="Content/Texts/TraditionalChinese/TraditionalChineseVanilla.xml" />
<Text file="Content/Texts/Japanese/JapaneseVanilla.xml" />
<Text file="Content/Texts/Turkish/TurkishVanilla.xml" />
<Text file="Content/Texts/Korean/KoreanVanilla.xml" />
<UIStyle file="Content/UI/style.xml"/>
<Afflictions file="Content/Afflictions.xml"/>
<Structure file="Content/Map/StructurePrefabs.xml" />
@@ -116,22 +121,24 @@
<LevelGenerationParameters file="Content/Map/LevelGenerationParameters.xml" />
<Missions file="Content/Missions.xml" />
<TraitorMissions file="Content/TraitorMissions.xml" />
<NPCConversations file="Content/NPCConversations/NpcConversations_English.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_German.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_French.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_Russian.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_BrazilianPortuguese.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_SimplifiedChinese.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_TraditionalChinese.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_CastilianSpanish.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_Japanese.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_LatinamericanSpanish.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_Polish.xml"/>
<NPCConversations file="Content/NPCConversations/NpcConversations_Turkish.xml"/>
<NPCConversations file="Content/NPCConversations/English/NpcConversations_English.xml"/>
<NPCConversations file="Content/NPCConversations/German/NpcConversations_German.xml"/>
<NPCConversations file="Content/NPCConversations/French/NpcConversations_French.xml"/>
<NPCConversations file="Content/NPCConversations/Russian/NpcConversations_Russian.xml"/>
<NPCConversations file="Content/NPCConversations/Polish/NpcConversations_Polish.xml"/>
<NPCConversations file="Content/NPCConversations/BrazilianPortuguese/NpcConversations_BrazilianPortuguese.xml"/>
<NPCConversations file="Content/NPCConversations/CastilianSpanish/NpcConversations_CastilianSpanish.xml"/>
<NPCConversations file="Content/NPCConversations/LatinamericanSpanish/NpcConversations_LatinamericanSpanish.xml"/>
<NPCConversations file="Content/NPCConversations/SimplifiedChinese/NpcConversations_SimplifiedChinese.xml"/>
<NPCConversations file="Content/NPCConversations/TraditionalChinese/NpcConversations_TraditionalChinese.xml"/>
<NPCConversations file="Content/NPCConversations/Japanese/NpcConversations_Japanese.xml"/>
<NPCConversations file="Content/NPCConversations/Turkish/NpcConversations_Turkish.xml"/>
<NPCConversations file="Content/NPCConversations/Korean/NpcConversations_Korean.xml"/>
<Jobs file="Content/Jobs.xml" />
<Orders file="Content/Orders.xml" />
<Sounds file="Content/Sounds/sounds.xml" />
<Tutorials file="Content/Tutorials/Tutorials.xml" />
<Executable file="Barotrauma.exe" />
<ServerExecutable file="DedicatedServer.exe" />
<SkillSettings file="Content/SkillSettings.xml" />
<Executable file="Barotrauma" />
<ServerExecutable file="DedicatedServer" />
</contentpackage>
@@ -6,8 +6,8 @@
karmadecaythreshold="50"
karmaincrease="0.05"
karmaincreasethreshold="50"
structurerepairkarmaincrease="0.05"
structuredamagekarmadecrease="0.08"
structurerepairkarmaincrease="0.01"
structuredamagekarmadecrease="0.05"
itemrepairkarmaincrease="0.03"
reactoroverheatkarmadecrease="0.5"
reactormeltdownkarmadecrease="30"
@@ -29,8 +29,8 @@
karmadecaythreshold="50"
karmaincrease="0.04"
karmaincreasethreshold="45"
structurerepairkarmaincrease="0.05"
structuredamagekarmadecrease="0.15"
structurerepairkarmaincrease="0.01"
structuredamagekarmadecrease="0.2"
itemrepairkarmaincrease="0.03"
reactoroverheatkarmadecrease="1.0"
reactormeltdownkarmadecrease="35"
@@ -52,8 +52,8 @@
karmadecaythreshold="50"
karmaincrease="0.05"
karmaincreasethreshold="50"
structurerepairkarmaincrease="0.05"
structuredamagekarmadecrease="0.08"
structurerepairkarmaincrease="0.01"
structuredamagekarmadecrease="0.05"
itemrepairkarmaincrease="0.03"
reactoroverheatkarmadecrease="0.5"
reactormeltdownkarmadecrease="30"
@@ -1,26 +0,0 @@
/* MonoKickstart - Kick Binaries for Mono Applications
*
* Copyright (c) 2012 Edward Rudd.
* Modified in 2013 by Ethan Lee.
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* Ethan "flibitijibibo" Lee <flibitijibibo@flibitijibibo.com>
*
*/
@@ -1,290 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>561357c2-db28-4e01-b275-6bf545f70491</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>BarotraumaShared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AICharacter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AIChatMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\AIController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\AITarget.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\EnemyAIController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\HumanAIController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\IndoorsSteeringManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\ISteerable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\LatchOntoAI.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\NPCConversation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjective.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveChargeBatteries.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveCombat.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveContainItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveDecontainItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveExtinguishFire.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveExtinguishFires.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveFindDivingGear.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveFindSafety.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveFixLeak.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveFixLeaks.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveGetItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveGoTo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveIdle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveLoop.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveOperateItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveFightIntruders.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectivePumpWater.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRepairItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRepairItems.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRescue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Objectives\AIObjectiveRescueAll.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\Order.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\PathFinder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\SteeringManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\SteeringPath.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\AI\SwarmBehavior.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\AnimController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\FishAnimController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\HumanoidAnimController.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Params\Animation\AnimationParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Params\Animation\FishAnimations.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Params\Animation\HumanoidAnimations.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Params\CharacterParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Params\EditableParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Params\Ragdoll\RagdollParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Animation\Ragdoll.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Attack.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\CauseOfDeath.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Character.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\CharacterInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\CharacterNetworking.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\Afflictions\Affliction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\Afflictions\AfflictionBleeding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\Afflictions\AfflictionHusk.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\Afflictions\AfflictionPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\Afflictions\AfflictionPsychosis.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\Afflictions\AfflictionSpaceHerpes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\Buffs\BuffDurationIncrease.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\CharacterHealth.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Health\DamageModifier.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Jobs\Job.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Jobs\JobPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Jobs\Skill.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Jobs\SkillPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\Limb.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Characters\NPCPersonalityTrait.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\ContentPackage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\CoroutineManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\DebugConsole.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\ArtifactEvent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\EventManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\EventManagerSettings.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\MalfunctionEvent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\Missions\CargoMission.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\Missions\CombatMission.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\Missions\Mission.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\Missions\MissionPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\Missions\MonsterMission.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\Missions\SalvageMission.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\MonsterEvent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\ScriptedEvent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\ScriptedEventPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Events\ScriptedEventSet.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Extensions\IEnumerableExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Extensions\PointExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Extensions\RectangleExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Extensions\StringFormatter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Extensions\VectorExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\FrameCounter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameAnalyticsManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\CargoManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\CrewManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\GameModes\CampaignMode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\GameModes\CharacterCampaignData.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\GameModes\GameMode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\GameModes\GameModePreset.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\GameModes\MissionMode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\GameModes\MultiPlayerCampaign.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\GameSession.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSession\HireManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\GameSettings.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\CharacterInventory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\DockingPort.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Door.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\ElectricalDischarger.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\Holdable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\LevelResource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\MeleeWeapon.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\Pickable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\Propulsion.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\RangedWeapon.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\RepairTool.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Holdable\Throwable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\ItemComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\ItemContainer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\ItemLabel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Ladder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Controller.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Deconstructor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Engine.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Fabricator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\MiniMap.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\OxygenGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Pump.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Reactor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Sonar.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\SonarTransducer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Steering.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Machines\Vent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Power\PowerContainer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Power\Powered.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Power\PowerTransfer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Projectile.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Repairable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Rope.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\AdderComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\AndComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\ColorComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\Connection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\ConnectionPanel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\CustomInterface.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\DelayComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\DivideComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\EqualsComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\FunctionComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\GreaterComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\LightComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\MemoryComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\ModuloComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\MotionSensor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\MultiplyComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\NotComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\OrComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\OscillatorComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\OxygenDetector.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\RegExFindComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\RelayComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\SignalCheckComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\SmokeDetector.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\SubtractComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\TrigonometricFunctionComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\WaterDetector.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\WifiComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\Wire.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Signal\XorComponent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\StatusHUD.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Turret.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Components\Wearable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Inventory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\Item.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\ItemInventory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\ItemPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Items\RelatedItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Entity.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\EntityGrid.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Explosion.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\DummyFireSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\FireSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Gap.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Hull.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\IDamageable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\ISpatialEntity.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\ItemAssemblyPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\CaveGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\Level.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\LevelGenerationParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\LevelObjects\LevelObject.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\LevelObjects\LevelObjectManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\LevelObjects\LevelObjectPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\LevelObjects\LevelTrigger.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\LevelWall.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\Ruins\BTRoom.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\Ruins\Corridor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\Ruins\RuinGenerationParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Levels\Ruins\RuinGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\LinkedSubmarine.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\MapEntity.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\MapEntityPrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Map\Location.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Map\LocationConnection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Map\LocationType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Map\LocationTypeChange.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Map\Map.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Map\MapGenerationParams.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Md5Hash.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\PriceInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\RoundEndCinematic.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Structure.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\StructurePrefab.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\Submarine.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\SubmarineBody.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Map\WayPoint.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Memento.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\BanList.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\ChatMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Client.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\ClientPermissions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\EntitySpawner.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\FileTransfer\FileTransfer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\INetSerializable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\KarmaManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\NetBufferExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\NetConfig.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\NetEntityEvent\NetEntityEvent.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\NetEntityEvent\NetEntityEventManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\NetIdUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\NetworkMember.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\OrderChatMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Primitives\Enums.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Primitives\Message\IReadMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Primitives\Message\IWriteMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Primitives\NetworkConnection\NetworkConnection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Primitives\NetworkConnection\LidgrenConnection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Primitives\Message\Message.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Primitives\NetworkConnection\SteamP2PConnection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\RespawnManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\ServerLog.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\ServerSettings.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\SteamManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Voip\VoipConfig.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Voip\VoipQueue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\Voting.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Networking\WhiteList.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Physics\Physics.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Physics\PhysicsBody.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\PlayerInput.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\ProcGen\PerlinNoise.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\ProcGen\Voronoi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\ProcGen\VoronoiElements.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Screens\GameScreen.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Screens\NetLobbyScreen.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Screens\Screen.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Serialization\ISerializableEntity.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Serialization\SerializableProperty.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Serialization\XMLExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Sprite\ConditionalSprite.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Sprite\DeformableSprite.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Sprite\Sprite.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Sprite\SpriteSheet.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\StatusEffects\DelayedEffect.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\StatusEffects\PropertyConditional.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\StatusEffects\StatusEffect.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\SteamAchievementManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\TextManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\TextPack.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Timing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\Homoglyphs.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\IPExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\MathUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\MTRandom.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\Rand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\SaveUtil.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\TaskPool.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\ToolBox.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source\Utils\UpdaterUtil.cs" />
</ItemGroup>
</Project>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>561357c2-db28-4e01-b275-6bf545f70491</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
<ReleaseVersion>0.9.0.0</ReleaseVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="SharedCode.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>false</ShowAllFiles>
</PropertyGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>a67d00eb-8f3d-49c9-9e01-94ed90b6bf2b</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
<ReleaseVersion>0.9.0.0</ReleaseVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="SharedContent.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>true</ShowAllFiles>
</PropertyGroup>
</Project>
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee }
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive }
abstract partial class AIController : ISteerable
{
@@ -12,7 +12,12 @@ namespace Barotrauma
public readonly Character Character;
private AIState state;
private AIState previousState;
protected void ResetAITarget()
{
_lastAiTarget = null;
_selectedAiTarget = null;
}
// Update only when the value changes, not when it keeps the same.
protected AITarget _lastAiTarget;
@@ -32,6 +37,7 @@ namespace Barotrauma
{
_lastAiTarget = _previousAiTarget;
}
OnTargetChanged(_previousAiTarget, _selectedAiTarget);
}
}
}
@@ -85,13 +91,13 @@ namespace Barotrauma
set
{
if (state == value) { return; }
previousState = state;
PreviousState = state;
OnStateChanged(state, value);
state = value;
}
}
public AIState PreviousState => previousState;
public AIState PreviousState { get; protected set; }
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
@@ -137,6 +143,7 @@ namespace Barotrauma
}
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
}
}
@@ -9,10 +9,14 @@ namespace Barotrauma
{
public static List<AITarget> List = new List<AITarget>();
private Entity entity;
public Entity Entity
{
get;
private set;
get
{
if (entity != null && entity.Removed) { return null; }
return entity;
}
}
private float soundRange;
@@ -21,20 +25,38 @@ namespace Barotrauma
/// <summary>
/// How long does it take for the ai target to fade out if not kept alive.
/// </summary>
public float FadeOutTime { get; private set; } = 1;
public float FadeOutTime { get; private set; } = 2;
public bool Static { get; private set; }
public bool StaticSound { get; private set; }
public bool StaticSight { get; private set; }
public float SoundRange
{
get { return soundRange; }
set { soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); }
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace);
return;
}
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
}
}
public float SightRange
{
get { return sightRange; }
set { sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); }
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace);
return;
}
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
}
}
private float sectorRad = MathHelper.TwoPi;
@@ -54,7 +76,7 @@ namespace Barotrauma
{
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + Entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
sectorDir = value;
@@ -68,8 +90,9 @@ namespace Barotrauma
}
public string SonarLabel;
public string SonarIconIdentifier;
public bool Enabled = true;
public bool Enabled => SoundRange > 0 || SightRange > 0;
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
@@ -87,7 +110,7 @@ namespace Barotrauma
{
get
{
if (Entity == null || Entity.Removed)
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
@@ -98,7 +121,7 @@ namespace Barotrauma
return Vector2.Zero;
}
return Entity.WorldPosition;
return entity.WorldPosition;
}
}
@@ -106,7 +129,7 @@ namespace Barotrauma
{
get
{
if (Entity == null || Entity.Removed)
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
@@ -117,7 +140,22 @@ namespace Barotrauma
return Vector2.Zero;
}
return Entity.SimPosition;
return entity.SimPosition;
}
}
public void Reset()
{
if (Static)
{
SightRange = MaxSightRange;
SoundRange = MaxSoundRange;
}
else
{
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
SightRange = StaticSight ? MaxSightRange : MinSightRange;
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
}
}
@@ -131,43 +169,66 @@ namespace Barotrauma
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange);
FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime);
Static = element.GetAttributeBool("static", Static);
StaticSight = element.GetAttributeBool("staticsight", StaticSight);
StaticSound = element.GetAttributeBool("staticsound", StaticSound);
if (Static)
{
SightRange = MaxSightRange;
SoundRange = MaxSoundRange;
StaticSound = true;
StaticSight = true;
}
else
{
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
SightRange = MinSightRange;
SoundRange = MinSoundRange;
}
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
string typeString = element.GetAttributeString("type", "Any");
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
string typeString = element.GetAttributeString("type", "Any");
if (Enum.TryParse(typeString, out TargetType t))
{
Type = t;
}
Reset();
}
public AITarget(Entity e)
{
Entity = e;
entity = e;
List.Add(this);
}
public void Update(float deltaTime)
{
if (!Static && FadeOutTime > 0)
if (Enabled && !Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
SightRange -= deltaTime * (MaxSightRange / FadeOutTime);
SoundRange -= deltaTime * (MaxSoundRange / FadeOutTime);
if (!StaticSight)
{
DecreaseSightRange(deltaTime);
}
if (!StaticSound)
{
DecreaseSoundRange(deltaTime);
}
}
}
public void IncreaseSoundRange(float deltaTime, float speed = 1)
{
SoundRange += speed * deltaTime * (MaxSoundRange / FadeOutTime);
}
public void IncreaseSightRange(float deltaTime, float speed = 1)
{
SightRange += speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public void DecreaseSoundRange(float deltaTime, float speed = 1)
{
SoundRange -= speed * deltaTime * (MaxSoundRange / FadeOutTime);
}
public void DecreaseSightRange(float deltaTime, float speed = 1)
{
SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) return true;
@@ -179,7 +240,7 @@ namespace Barotrauma
public void Remove()
{
List.Remove(this);
Entity = null;
entity = null;
}
}
}
@@ -20,7 +20,7 @@ namespace Barotrauma
private float unreachableClearTimer;
private bool shouldCrouch;
const float reactionTime = 0.5f;
const float reactionTime = 0.3f;
const float crouchRaycastInterval = 1;
const float sortObjectiveInterval = 1;
const float clearUnreachableInterval = 30;
@@ -55,6 +55,8 @@ namespace Barotrauma
private set;
}
public float CurrentHullSafety { get; private set; }
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
@@ -83,11 +85,12 @@ namespace Barotrauma
{
unreachableClearTimer = clearUnreachableInterval;
UnreachableHulls.Clear();
ignoredContainers.Clear();
}
float maxDistanceToSub = 3000;
if (Character.Submarine != null || SelectedAiTarget?.Entity?.Submarine != null &&
Vector2.DistanceSquared(Character.WorldPosition, SelectedAiTarget.Entity.Submarine.WorldPosition) < maxDistanceToSub * maxDistanceToSub)
// Use the pathfinding also outside of the sub, but not farther than the extents of the sub + 500 units.
if (Character.Submarine != null || SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null &&
Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + 500, 2))
{
if (steeringManager != insideSteering)
{
@@ -122,6 +125,11 @@ namespace Barotrauma
if (reactTimer > 0.0f)
{
reactTimer -= deltaTime;
if (findItemState != FindItemState.None)
{
// Update every frame only when seeking items
UnequipUnnecessaryItems();
}
}
else
{
@@ -134,6 +142,7 @@ namespace Barotrauma
ReportProblems();
UpdateSpeaking();
}
UnequipUnnecessaryItems();
reactTimer = reactionTime * Rand.Range(0.75f, 1.25f);
}
@@ -236,11 +245,6 @@ namespace Barotrauma
Character.AnimController.TargetMovement = targetMovement;
if (!Character.LockHands)
{
UnequipUnnecessaryItems();
}
flipTimer -= deltaTime;
if (flipTimer <= 0.0f)
{
@@ -272,6 +276,8 @@ namespace Barotrauma
private void UnequipUnnecessaryItems()
{
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
if (findItemState == FindItemState.None || findItemState == FindItemState.Extinguisher)
{
@@ -293,7 +299,7 @@ namespace Barotrauma
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
@@ -317,7 +323,7 @@ namespace Barotrauma
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow;
AIObjectiveGoTo gotoObjective = ObjectiveManager.CurrentOrder as AIObjectiveGoTo;
AIObjectiveGoTo gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
if (!removeDivingSuit)
{
bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character);
@@ -376,8 +382,14 @@ namespace Barotrauma
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
{
DropIfFailsToContain = false
};
decontainObjective.Abandoned += () =>
{
ignoredContainers.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -412,7 +424,7 @@ namespace Barotrauma
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
@@ -431,42 +443,38 @@ namespace Barotrauma
}
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>() ||
ObjectiveManager.IsCurrentObjective<AIObjectiveOperateItem>() ||
ObjectiveManager.IsCurrentObjective<AIObjectivePumpWater>() ||
ObjectiveManager.IsCurrentObjective<AIObjectiveChargeBatteries>())
if (!ObjectiveManager.CurrentObjective.UnequipItems || !ObjectiveManager.GetActiveObjective().UnequipItems) { return; }
if (ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
foreach (var item in Character.Inventory.Items)
{
foreach (var item in Character.Inventory.Items)
if (item == null) { continue; }
if (Character.HasEquippedItem(item) &&
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
{
if (item == null) { continue; }
if (Character.HasEquippedItem(item) &&
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (FindSuitableContainer(Character, item, out Item targetContainer))
{
if (FindSuitableContainer(Character, item, out Item targetContainer))
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, targetContainer.GetComponent<ItemContainer>(), ObjectiveManager, targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
item.Drop(Character);
}
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
findItemState = FindItemState.OtherItem;
item.Drop(Character);
}
}
else
{
findItemState = FindItemState.OtherItem;
}
}
}
}
@@ -496,13 +504,13 @@ namespace Barotrauma
{
if (isRestrictionsDefined)
{
return 3;
return 4;
}
else
{
if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined))
if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
{
return isPreferencesDefined ? 2 : 1;
return isPreferencesDefined ? isSecondary ? 2 : 3 : 1;
}
else
{
@@ -528,14 +536,16 @@ namespace Barotrauma
Hull targetHull = null;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
bool isFleeing = ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>();
foreach (var hull in VisibleHulls)
{
foreach (Character c in Character.CharacterList)
foreach (Character target in Character.CharacterList)
{
if (c.CurrentHull != hull || !c.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(c, Character))
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character))
{
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, c) && newOrder == null)
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = Order.GetPrefab("reportintruders");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -552,42 +562,48 @@ namespace Barotrauma
targetHull = hull;
}
}
foreach (Character c in Character.CharacterList)
if (!isFighting)
{
if (c.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(c, Character))
foreach (var gap in hull.ConnectedGaps)
{
if (AddTargets<AIObjectiveRescueAll, Character>(c, Character) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
if (AIObjectiveFixLeaks.IsValidTarget(gap, Character))
{
var orderPrefab = Order.GetPrefab("requestfirstaid");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
{
var orderPrefab = Order.GetPrefab("reportbreach");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
}
}
}
foreach (var gap in hull.ConnectedGaps)
{
if (AIObjectiveFixLeaks.IsValidTarget(gap, Character))
if (!isFleeing)
{
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
foreach (Character target in Character.CharacterList)
{
var orderPrefab = Order.GetPrefab("reportbreach");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
if (target.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(target, Character))
{
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
var orderPrefab = Order.GetPrefab("requestfirstaid");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
}
}
}
}
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
foreach (Item item in Item.ItemList)
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character);
targetHull = hull;
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character);
targetHull = hull;
}
}
}
}
}
@@ -642,7 +658,7 @@ namespace Barotrauma
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
if (!attacker.IsRemotePlayer && Character.Controlled != attacker && attacker.AIController != null && attacker.AIController.Enabled)
if (!attacker.IsPlayer && attacker.AIController != null && attacker.AIController.Enabled)
{
// Don't retaliate on damage done by friendly ai, because we know that it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
@@ -656,9 +672,8 @@ namespace Barotrauma
}
else
{
float currentVitality = Character.CharacterHealth.Vitality;
float dmgPercentage = damage / currentVitality * 100;
if (dmgPercentage < currentVitality / 10)
float dmgPercentage = MathUtils.Percentage(damage, Character.CharacterHealth.Vitality);
if (dmgPercentage < 10)
{
// Don't retaliate on minor (accidental) dmg done by characters that are in the same team
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
@@ -741,11 +756,8 @@ namespace Barotrauma
Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
}
}
SetOrderProjSpecific(order);
}
partial void SetOrderProjSpecific(Order order);
public override void SelectTarget(AITarget target)
{
SelectedAiTarget = target;
@@ -798,11 +810,11 @@ namespace Barotrauma
/// </summary>
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", "oxygensource", conditionPercentage);
public static bool HasItem(Character character, string identifier, string containedTag, float conditionPercentage = 0)
public static bool HasItem(Character character, string tagOrIdentifier, string containedTag = null, float conditionPercentage = 0)
{
if (character == null) { return false; }
if (character.Inventory == null) { return false; }
var item = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
var item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
return item != null &&
item.ConditionPercentage > conditionPercentage &&
character.HasEquippedItem(item) &&
@@ -811,19 +823,6 @@ namespace Barotrauma
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
}
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action)
{
if (character == null) { return; }
foreach (var c in Character.CharacterList)
{
if (c == null || c.IsDead || c.Removed) { continue; }
if (c.AIController is HumanAIController humanAi && humanAi.IsFriendly(character))
{
action(humanAi);
}
}
}
/// <summary>
/// Updates the hull safety for all ai characters in the team.
/// </summary>
@@ -832,7 +831,6 @@ namespace Barotrauma
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull));
}
public float CurrentHullSafety { get; private set; }
private void RefreshHullSafety(Hull hull)
{
if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD)
@@ -867,7 +865,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { continue; }
if (item.Repairables.All(r => item.ConditionPercentage >= r.AIRepairThreshold)) { continue; }
AddTargets<AIObjectiveRepairItems, Item>(character, item);
}
}
@@ -923,8 +921,6 @@ namespace Barotrauma
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
public float GetCurrentHullSafety() => GetHullSafety(Character.CurrentHull, Character, VisibleHulls);
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
bool isCurrentHull = character == Character && character.CurrentHull == hull;
@@ -942,7 +938,7 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
// TODO: should we calculate the visible hulls for each hull? -> could be a bit heavy.
bool ignoreFire = ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreFire = objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
@@ -968,7 +964,7 @@ namespace Barotrauma
float fireFactor = 1;
if (!ignoreFire)
{
Func<Hull, float> calculateFire = h => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
// Even the smallest fire reduces the safety by 50%
float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h));
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
@@ -976,10 +972,10 @@ namespace Barotrauma
float enemyFactor = 1;
if (!ignoreEnemies)
{
Func<Character, bool> isValidTarget = e => IsActive(e) && !IsFriendly(character, e);
bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e);
int enemyCount = visibleHulls == null ?
Character.CharacterList.Count(e => e.CurrentHull == hull && isValidTarget(e)) :
Character.CharacterList.Count(e => visibleHulls.Contains(e.CurrentHull) && isValidTarget(e));
Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) :
Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull));
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
}
@@ -989,13 +985,92 @@ namespace Barotrauma
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public static bool IsFriendly(Character me, Character other)
{
bool sameSpecies = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
bool differentTeam = me.TeamID == Character.TeamType.Team1 && other.TeamID == Character.TeamType.Team2 || me.TeamID == Character.TeamType.Team2 && other.TeamID == Character.TeamType.Team1;
return sameSpecies && !differentTeam;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
{
if (!predicate(c.AIController as HumanAIController))
{
return false;
}
}
}
return true;
}
public static bool IsTrueForAnyCrewMember(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
{
if (predicate(c.AIController as HumanAIController))
{
return true;
}
}
}
return false;
}
public static int CountCrew(Character character, Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false)
{
if (character == null) { return 0; }
int count = 0;
foreach (var other in Character.CharacterList)
{
if (onlyActive && !IsActive(other))
{
continue;
}
if (onlyBots && other.IsPlayer)
{
continue;
}
if (FilterCrewMember(character, other))
{
if (predicate == null || predicate(other.AIController as HumanAIController))
{
count++;
}
}
}
return count;
}
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action)
{
if (character == null) { return; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
{
action(c.AIController as HumanAIController);
}
}
}
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
#region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public static bool IsFriendly(Character me, Character other) =>
(other.TeamID == me.TeamID ||
other.TeamID == Character.TeamType.FriendlyNPC ||
me.TeamID == Character.TeamType.FriendlyNPC) && (other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group));
public static bool IsActive(Character other) => !other.Removed && !other.IsDead && !other.IsUnconscious;
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action);
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
#endregion
}
}
@@ -56,32 +56,9 @@ namespace Barotrauma
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsNextNodeLadder
{
get
{
if (currentPath == null) { return false; }
if (currentPath.NextNode == null) { return false; }
if (currentPath.NextNode.Ladders != null)
{
return true;
}
else
{
// Check if the node after the next node is ladder.
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return false; }
return node.Ladders != null;
}
return false;
}
}
}
public bool IsNextNodeLadder => GetNextLadder() != null;
public bool IsNextLadderSameAsCurrent
{
@@ -92,31 +69,14 @@ namespace Barotrauma
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder == null) { return false; }
var nextLadder = currentPath.NextNode.Ladders;
if (nextLadder != null)
{
return currentLadder == nextLadder;
}
else
{
// Check if the node after the next node is in the same ladder as the current.
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return false; }
nextLadder = node.Ladders;
bool isSame = nextLadder != null && nextLadder == currentLadder;
return isSame;
}
return false;
}
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
}
}
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true);
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), indoorsSteering: true);
pathFinder.GetNodePenalty = GetNodePenalty;
this.canOpenDoors = canOpenDoors;
@@ -148,9 +108,33 @@ namespace Barotrauma
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter);
}
/// <summary>
/// Seeks the ladder from the current and the next two nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null)
{
return currentPath.NextNode.Ladders;
}
else
{
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
return node.Ladders;
}
return null;
}
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
bool needsNewPath = currentPath == null || (currentPath.Unreachable || currentPath.NextNode == null) || Vector2.DistanceSquared(target, currentTarget) > 1;
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1);
//find a new path if one hasn't been found yet or the target is different from the current target
if (needsNewPath || findPathTimer < -1.0f)
{
@@ -160,12 +144,13 @@ namespace Barotrauma
Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null)
{
var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false);
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
currentPos -= targetHull.Submarine.SimPosition;
}
}
pathFinder.InsideSubmarine = character.Submarine != null;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter);
bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
@@ -179,7 +164,8 @@ namespace Barotrauma
{
currentPath = newPath;
}
findPathTimer = Rand.Range(1.0f, 1.2f);
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
}
@@ -230,7 +216,7 @@ namespace Barotrauma
}
else if (character.Submarine != currentPath.CurrentNode.Submarine)
{
pos -= FarseerPhysics.ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position);
pos -= ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position);
}
}
}
@@ -251,14 +237,13 @@ namespace Barotrauma
if (character.IsClimbing && !isDiving)
{
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
Ladder nextLadder = GetNextLadder();
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
{
@@ -267,20 +252,29 @@ namespace Barotrauma
{
diff.Y = Math.Max(diff.Y, 1.0f);
}
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
else if (!nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition))
{
nextLadder.Item.TryInteract(character, false, true);
}
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool aboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
if (aboveFloor || IsNextNodeLadder)
bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
if (nextLadder != null || isAboveFloor)
{
if (!nextLadderSameAsCurrent || currentPath.NextNode == null && aboveFloor)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
currentPath.SkipToNextNode();
}
}
else if (nextLadderSameAsCurrent)
else if (nextLadder != null)
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
@@ -302,12 +296,19 @@ namespace Barotrauma
}
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(targetDistance, 2))
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
{
verticalDistance *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
}
}
else
else if (!IsNextLadderSameAsCurrent)
{
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
@@ -340,11 +341,11 @@ namespace Barotrauma
if (!canOpenDoors || character.LockHands) { return false; }
if (door.HasIntegratedButtons)
{
return door.HasRequiredItems(character, false);
return door.CanBeOpenedWithoutTools(character);
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasRequiredItems(character, false) && (buttonFilter == null || buttonFilter(b)));
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
}
}
@@ -529,7 +530,6 @@ namespace Barotrauma
if (node.Waypoint != null && node.Waypoint.CurrentHull != null)
{
var hull = node.Waypoint.CurrentHull;
if (hull.FireSources.Count > 0)
{
foreach (FireSource fs in hull.FireSources)
@@ -537,9 +537,14 @@ namespace Barotrauma
penalty += fs.Size.X * 10.0f;
}
}
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) penalty += 500.0f;
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) penalty += 1000.0f;
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
{
penalty += 500.0f;
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
penalty += 1000.0f;
}
}
return penalty;
@@ -600,10 +605,7 @@ namespace Barotrauma
if (wander)
{
SteeringWander();
if (currentHull == null)
{
SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance));
}
SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5);
}
if (!inWater)
{
@@ -269,7 +269,7 @@ namespace Barotrauma
KinematicBodyB = true,
CollideConnected = false,
};
GameMain.World.AddJoint(limbJoint);
GameMain.World.Add(limbJoint);
attachJoints.Add(limbJoint);
// Limb scale is already taken into account when creating the collider.
@@ -285,7 +285,7 @@ namespace Barotrauma
CollideConnected = false,
//Length = 0.1f
};
GameMain.World.AddJoint(colliderJoint);
GameMain.World.Add(colliderJoint);
attachJoints.Add(colliderJoint);
}
@@ -293,7 +293,7 @@ namespace Barotrauma
{
foreach (Joint joint in attachJoints)
{
GameMain.World.RemoveJoint(joint);
GameMain.World.Remove(joint);
}
attachJoints.Clear();
}
@@ -10,8 +10,47 @@ namespace Barotrauma
{
const int MaxPreviousConversations = 20;
private static List<NPCConversation> list = new List<NPCConversation>();
private class ConversationCollection
{
public readonly string Identifier;
public readonly Dictionary<string, List<NPCConversation>> Conversations;
public ConversationCollection(string identifier)
{
Identifier = identifier;
Conversations = new Dictionary<string, List<NPCConversation>>();
}
public void Add(string language, string filePath, XElement subElement)
{
if (!Conversations.ContainsKey(language))
{
Conversations.Add(language, new List<NPCConversation>());
}
Conversations[language].Add(new NPCConversation(subElement, filePath));
}
public void RemoveByFile(string filePath)
{
List<string> keysToRemove = new List<string>();
foreach (var kpv in Conversations)
{
kpv.Value.RemoveAll(c => c.FilePath == filePath);
if (kpv.Value.Count == 0) { keysToRemove.Add(kpv.Key); }
}
foreach (var key in keysToRemove)
{
Conversations.Remove(key);
}
}
}
private static Dictionary<string, ConversationCollection> allConversations = new Dictionary<string, ConversationCollection>();
public readonly string FilePath;
public readonly string Line;
public readonly List<JobPrefab> AllowedJobs;
@@ -25,75 +64,70 @@ namespace Barotrauma
public readonly List<NPCConversation> Responses;
private readonly int speakerIndex;
private readonly List<string> allowedSpeakerTags;
public static void LoadAll(IEnumerable<string> filePaths)
public static void LoadAll(IEnumerable<ContentFile> files)
{
//language, identifier, filepath
List<Tuple<string, string, string>> contentPackageFiles = new List<Tuple<string, string, string>>();
foreach (string filePath in filePaths)
foreach (var file in files)
{
if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { continue; }
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
contentPackageFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
}
List<Tuple<string, string, string>> translationFiles = new List<Tuple<string, string, string>>();
foreach (string filePath in Directory.GetFiles(Path.Combine("Content", "NPCConversations")))
{
if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { continue; }
string language = doc.Root.GetAttributeString("Language", "English");
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
translationFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
}
//get the languages and identifiers of the files
for (int i = 0; i < contentPackageFiles.Count; i++)
{
var contentPackageFile = contentPackageFiles[i];
//correct language, all good
if (contentPackageFile.Item1 == TextManager.Language) continue;
//attempt to find a translation file with the correct language and a matching identifier
//if it fails, we'll just use the original file with the incorrect language
var translation = translationFiles.Find(t => t.Item1 == TextManager.Language && t.Item2 == contentPackageFile.Item2);
if (translation != null) contentPackageFiles[i] = translation; //replace with the translation file
}
foreach (var file in contentPackageFiles)
{
Load(file.Item3);
if (Path.GetExtension(file.Path) == ".csv") continue; // .csv files are not supported
LoadFromFile(file);
}
}
private static void Load(string file)
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
string language = doc.Root.GetAttributeString("Language", "English");
if (language != TextManager.Language) return;
string identifier = doc.Root.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Conversations file '{file.Path}' has no identifier!");
return;
}
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conversation":
list.Add(new NPCConversation(subElement));
if (!allConversations.ContainsKey(identifier))
{
allConversations.Add(identifier, new ConversationCollection(identifier));
}
allConversations[identifier].Add(language, file.Path, subElement);
break;
case "personalitytrait":
new NPCPersonalityTrait(subElement);
new NPCPersonalityTrait(subElement, file.Path);
break;
}
}
}
public NPCConversation(XElement element)
public static void RemoveByFile(string filePath)
{
List<string> keysToRemove = new List<string>();
foreach (var kpv in allConversations)
{
kpv.Value.RemoveByFile(filePath);
if (!kpv.Value.Conversations.Any())
{
keysToRemove.Add(kpv.Key);
}
}
foreach (string key in keysToRemove)
{
allConversations.Remove(key);
}
NPCPersonalityTrait.List.RemoveAll(npt => npt.FilePath == filePath);
}
public NPCConversation(XElement element, string filePath)
{
FilePath = filePath;
Line = element.GetAttributeString("line", "");
speakerIndex = element.GetAttributeInt("speaker", 0);
@@ -102,9 +136,10 @@ namespace Barotrauma
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
{
if (JobPrefab.List.TryGetValue(allowedJobIdentifier.ToLowerInvariant(), out JobPrefab jobPrefab))
string key = allowedJobIdentifier.ToLowerInvariant();
if (JobPrefab.Prefabs.ContainsKey(key))
{
AllowedJobs.Add(jobPrefab);
AllowedJobs.Add(JobPrefab.Prefabs[key]);
}
}
@@ -124,7 +159,7 @@ namespace Barotrauma
Responses = new List<NPCConversation>();
foreach (XElement subElement in element.Elements())
{
Responses.Add(new NPCConversation(subElement));
Responses.Add(new NPCConversation(subElement, filePath));
}
}
@@ -167,7 +202,8 @@ namespace Barotrauma
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: list);
CreateConversation(availableSpeakers, assignedSpeakers, null, lines,
availableConversations: allConversations.Values.SelectMany(cc => cc.Conversations.Where(kpv => kpv.Key == TextManager.Language).SelectMany(kpv => kpv.Value)).ToList());
return lines;
}
@@ -175,7 +211,8 @@ namespace Barotrauma
{
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
var availableConversations = list.FindAll(conversation => requiredFlags.All(f => conversation.Flags.Contains(f)));
var availableConversations = allConversations.Values.SelectMany(cc => cc.Conversations.SelectMany(
kpv => kpv.Value.Where(conversation => kpv.Key == TextManager.Language && requiredFlags.All(f => conversation.Flags.Contains(f))))).ToList();
if (availableConversations.Count > 0)
{
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: true);
@@ -324,12 +361,18 @@ namespace Barotrauma
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < list.Count; i++)
foreach (string key in allConversations.Keys)
{
NPCConversation current = list[i];
WriteConversation(sb, current, 0);
WriteSubConversations(sb, current.Responses, 1);
WriteEmptyRow(sb);
foreach (string lang in allConversations[key].Conversations.Keys)
{
if (lang != TextManager.Language) { continue; }
foreach (var current in allConversations[key].Conversations[lang])
{
WriteConversation(sb, current, 0);
WriteSubConversations(sb, current.Responses, 1);
WriteEmptyRow(sb);
}
}
}
StreamWriter file = new StreamWriter(@"NPCConversations.csv");
@@ -15,7 +15,6 @@ namespace Barotrauma
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ReportFailures => true;
/// <summary>
/// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops.
@@ -30,8 +29,21 @@ namespace Barotrauma
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool UnequipItems => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
protected float CumulatedDevotion
{
get { return _cumulatedDevotion; }
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
}
protected virtual float MaxDevotion => 10;
/// <summary>
/// Final priority value after all calculations.
/// </summary>
public float Priority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
@@ -59,6 +71,7 @@ namespace Barotrauma
/// </summary>
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
@@ -86,7 +99,7 @@ namespace Barotrauma
public AIObjective GetActiveObjective()
{
var subObjective = SubObjectives.FirstOrDefault();
var subObjective = CurrentSubObjective;
return subObjective == null ? this : subObjective.GetActiveObjective();
}
@@ -157,7 +170,8 @@ namespace Barotrauma
{
if (!AllowSubObjectiveSorting) { return; }
if (subObjectives.None()) { return; }
subObjectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
subObjectives.ForEach(so => so.GetPriority());
subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
if (ConcurrentObjectives)
{
subObjectives.ForEach(so => so.SortSubObjectives());
@@ -168,7 +182,23 @@ namespace Barotrauma
}
}
public virtual float GetPriority() => Priority * PriorityModifier;
/// <summary>
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
/// </summary>
public virtual float GetPriority()
{
Priority = CumulatedDevotion * PriorityModifier;
return Priority;
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
{
CumulatedDevotion += Devotion * PriorityModifier * deltaTime;
}
}
public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
@@ -180,14 +210,7 @@ namespace Barotrauma
}
else if (objectiveManager.WaitTimer <= 0)
{
if (objectiveManager.CurrentObjective != null)
{
if (objectiveManager.CurrentObjective == this || objectiveManager.CurrentObjective.subObjectives.Any(so => so == this))
{
Priority += Devotion * PriorityModifier * deltaTime;
}
}
Priority = MathHelper.Clamp(Priority, 0, 100);
UpdateDevotion(deltaTime);
}
subObjectives.ForEach(so => so.Update(deltaTime));
}
@@ -264,6 +287,7 @@ namespace Barotrauma
public virtual void OnDeselected()
{
CumulatedDevotion = 0;
Deselected?.Invoke();
}
@@ -282,6 +306,7 @@ namespace Barotrauma
isCompleted = false;
hasBeenChecked = false;
_abandon = false;
CumulatedDevotion = 0;
}
protected abstract void Act(float deltaTime);
@@ -10,6 +10,7 @@ namespace Barotrauma
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{
public override string DebugTag => "charge batteries";
public override bool UnequipItems => true;
private IEnumerable<PowerContainer> batteryList;
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
@@ -100,7 +100,11 @@ namespace Barotrauma
}
}
public override float GetPriority() => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
public override float GetPriority()
{
Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
return Priority;
}
public override void Update(float deltaTime)
{
@@ -139,7 +143,7 @@ namespace Barotrauma
}
if (seekAmmunition == null)
{
if (TryArm())
if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed)
{
OperateWeapon(deltaTime);
}
@@ -419,10 +423,24 @@ namespace Barotrauma
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
}
if (character.CurrentHull != retreatTarget)
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
onAbandon: () => Abandon = true,
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref retreatObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref retreatObjective));
}
}
@@ -446,7 +464,9 @@ namespace Barotrauma
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
{
IgnoreIfTargetDead = true
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName
},
onAbandon: () =>
{
@@ -531,6 +551,10 @@ namespace Barotrauma
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(ammunition))
{
return false;
}
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
{
@@ -28,6 +28,8 @@ namespace Barotrauma
public bool AllowToFindDivingGear { get; set; } = true;
public float ConditionLevel { get; set; }
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -63,7 +65,7 @@ namespace Barotrauma
int containedItemCount = 0;
foreach (Item i in container.Inventory.Items)
{
if (i != null && itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)))
if (i != null && CheckItem(i))
{
containedItemCount++;
}
@@ -72,35 +74,53 @@ namespace Barotrauma
}
}
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
protected override void Act(float deltaTime)
{
if (container == null)
{
Abandon = true;
return;
}
Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
if (itemToContain != null)
{
// Contain the item
if (itemToContain.ParentInventory == character.Inventory)
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
{
character.Inventory.RemoveItem(itemToContain);
if (!container.Inventory.TryPutItem(itemToContain, null))
if (RemoveEmpty)
{
itemToContain.Drop(character);
Abandon = true;
foreach (var emptyItem in container.Inventory.Items)
{
if (emptyItem == null) { continue; }
if (emptyItem.Condition <= 0)
{
emptyItem.Drop(character);
}
}
}
}
else
{
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
// Contain the item
if (itemToContain.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(itemToContain))
{
Abandon = true;
}
else
{
character.Inventory.RemoveItem(itemToContain);
if (container.Inventory.TryPutItem(itemToContain, null))
{
IsCompleted = true;
}
else
{
itemToContain.Drop(character);
Abandon = true;
}
}
}
else
{
if (container.Combine(itemToContain, character))
{
@@ -111,19 +131,23 @@ namespace Barotrauma
Abandon = true;
}
}
else
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: false, checkInventory: checkInventory)
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
@@ -20,7 +20,16 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem containObjective;
public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
public bool Equip { get; set; }
/// <summary>
/// If true drops the item when containing the item fails.
/// In both cases abandons the objective.
/// Note that has no effect if the target container was not defined (always drops) -> completes when the item is dropped.
/// </summary>
public bool DropIfFailsToContain { get; set; } = true;
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetItem = targetItem;
@@ -28,10 +37,10 @@ namespace Barotrauma
this.targetContainer = targetContainer;
}
public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, sourceContainer, objectiveManager, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
@@ -45,15 +54,6 @@ namespace Barotrauma
protected override bool Check() => IsCompleted;
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
protected override void Act(float deltaTime)
{
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
@@ -64,6 +64,11 @@ namespace Barotrauma
}
if (targetContainer == null)
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (itemToDecontain.Container != sourceContainer.Item)
{
IsCompleted = true;
@@ -80,10 +85,21 @@ namespace Barotrauma
}
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
{
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager),
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager)
{
// If the container changes, the item is no longer where it was
abortCondition = () => itemToDecontain.Container != sourceContainer.Item,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = sourceContainer.Item.Name
},
onAbandon: () => Abandon = true);
return;
}
@@ -91,9 +107,22 @@ namespace Barotrauma
if (targetContainer != null)
{
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager) { GetItemPriority = this.GetItemPriority },
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
Equip = this.Equip,
RemoveEmpty = false,
GetItemPriority = this.GetItemPriority,
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
},
onCompleted: () => IsCompleted = true,
onAbandon: () => targetContainer = null);
onAbandon: () =>
{
if (DropIfFailsToContain)
{
itemToDecontain.Drop(character);
}
Abandon = true;
});
}
else
{
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
@@ -29,32 +28,44 @@ namespace Barotrauma
public override float GetPriority()
{
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
distanceFactor = 1;
Priority = 0;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + severityFactor * distanceFactor, 0, 1));
else
{
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override bool Check() => targetHull.FireSources.None();
private float sinTime;
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher");
var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
return new AIObjectiveGetItem(character, "extinguisher", objectiveManager, equip: true);
return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true)
{
// If the item is inside an unsafe hull, decrease the priority
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
};
});
}
else
@@ -79,8 +90,12 @@ namespace Barotrauma
{
useExtinquisherTimer = 0.0f;
}
// Aim
character.CursorPosition = fs.Position;
if (extinguisher.Item.RequireAimToUse)
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
float dist = fromCharacterToFireSource.Length();
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (extinguisherItem.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
@@ -95,26 +110,15 @@ namespace Barotrauma
{
character.SetInput(InputType.Aim, false, true);
}
sinTime += deltaTime * 10;
}
Limb sightLimb = null;
if (character.Inventory.IsInLimbSlot(extinguisherItem, InvSlotType.RightHand))
character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true);
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
sightLimb = character.AnimController.GetLimb(LimbType.RightHand);
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f);
}
else if (character.Inventory.IsInLimbSlot(extinguisherItem, InvSlotType.LeftHand))
{
sightLimb = character.AnimController.GetLimb(LimbType.LeftHand);
}
if (character.CanSeeTarget(fs, sightLimb))
{
character.SetInput(extinguisher.Item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.RoomName, true), null, 0, "putoutfire", 10.0f);
}
}
else
if (!character.CanSeeTarget(fs))
{
move = true;
}
@@ -122,7 +126,11 @@ namespace Barotrauma
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager),
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
@@ -9,7 +9,6 @@ namespace Barotrauma
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -9,7 +9,6 @@ namespace Barotrauma
public override string DebugTag => $"find diving gear ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly string gearTag;
private readonly string fallbackTag;
@@ -80,6 +79,10 @@ namespace Barotrauma
var container = item.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (!container.Inventory.CanBePut(oxygenTank))
{
Abandon = true;
}
character.Inventory.RemoveItem(oxygenTank);
if (!container.Inventory.TryPutItem(oxygenTank, null))
{
@@ -33,6 +33,8 @@ namespace Barotrauma
private bool resetPriority;
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (resetPriority)
@@ -145,9 +147,14 @@ namespace Barotrauma
},
onAbandon: () =>
{
if (currentHull != null)
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
if (currentHull != null && goToObjective != null)
{
HumanAIController.UnreachableHulls.Add(goToObjective.Target as Hull);
if (goToObjective.Target is Hull hull)
{
HumanAIController.UnreachableHulls.Add(hull);
}
}
RemoveSubObjective(ref goToObjective);
});
@@ -172,7 +179,7 @@ namespace Barotrauma
}
foreach (Character enemy in Character.CharacterList)
{
if (HumanAIController.IsFriendly(enemy) || !HumanAIController.IsActive(enemy)) { continue; }
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy)) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
@@ -247,7 +254,7 @@ namespace Barotrauma
else
{
// Outside
if (hull.RoomName != null && hull.RoomName.ToLowerInvariant().Contains("airlock"))
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
{
hullSafety = 100;
}
@@ -29,24 +29,31 @@ namespace Barotrauma
public override float GetPriority()
{
if (Leak.Removed || Leak.Open <= 0) { return 0; }
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + severity * distanceFactor * PriorityModifier, 0, 1));
if (Leak.Removed || Leak.Open <= 0)
{
Priority = 0;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItemByTag("weldingtool", true);
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true),
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
return;
@@ -115,9 +122,11 @@ namespace Barotrauma
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
AllowGoingOutside = objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>(),
CloseEnough = reach
},
AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName
},
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
@@ -11,7 +11,6 @@ namespace Barotrauma
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -24,7 +23,7 @@ namespace Barotrauma
float severity = sizeFactor * leak.Open;
if (!leak.IsRoomToRoom)
{
severity *= 100;
severity *= 10;
// If there is a leak in the outer walls, the severity cannot be lower than 10, no matter how small the leak
return MathHelper.Clamp(severity, 10, 100);
}
@@ -34,7 +33,31 @@ namespace Barotrauma
}
}
protected override float TargetEvaluation() => Targets.Max(t => GetLeakSeverity(t));
protected override float TargetEvaluation()
{
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>(), onlyBots: true);
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
{
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
}
else
{
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
}
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
}
}
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap)
=> new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier);
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -15,28 +14,23 @@ namespace Barotrauma
public HashSet<Item> ignoredItems = new HashSet<Item>();
public Func<Item, float> GetItemPriority;
public Func<Item, bool> ItemFilter;
public float TargetCondition { get; set; } = 1;
//can be either tags or identifiers
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
private Item targetItem, moveToTarget;
private Item targetItem, moveToTarget, rootContainer;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
public string[] ignoredContainerIdentifiers;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private bool checkInventory;
public bool AllowToFindDivingGear { get; set; } = true;
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -58,21 +52,20 @@ namespace Barotrauma
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
if (checkInventory)
{
CheckInventory();
}
this.checkInventory = checkInventory;
}
private void CheckInventory()
private bool CheckInventory()
{
if (itemIdentifiers == null) { return; }
var item = character.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.Condition > 0, recursive: true);
if (itemIdentifiers == null) { return false; }
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (item != null)
{
targetItem = item;
moveToTarget = item.GetRootContainer() ?? item;
rootContainer = item.GetRootContainer();
moveToTarget = rootContainer ?? item;
}
return item != null;
}
protected override void Act(float deltaTime)
@@ -82,28 +75,38 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetItem == null)
if (itemIdentifiers != null && !isDoneSeeking)
{
FindTargetItem();
if (targetItem == null || moveToTarget == null)
if (checkInventory)
{
if (targetItem != null && moveToTarget == null)
if (CheckInventory())
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Move to target is null!");
#endif
Abandon = true;
isDoneSeeking = true;
}
}
if (!isDoneSeeking)
{
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
return;
}
}
if (targetItem == null || targetItem.Removed)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
return;
}
if (character.IsItemTakenBySomeoneElse(targetItem))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else. Aborting.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else.", Color.Yellow);
#endif
Abandon = true;
// Try again
Reset();
return;
}
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
{
@@ -116,7 +119,7 @@ namespace Barotrauma
Abandon = true;
return;
}
targetItem.TryInteract(character, forceSelectKey: true);
if (equip)
{
int targetSlot = -1;
@@ -133,13 +136,18 @@ namespace Barotrauma
var otherItem = character.Inventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any })) { continue; }
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) &&
character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
//if everything else fails, simply drop the existing item
otherItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character))
{
targetItem.Equip(character);
IsCompleted = true;
}
else
@@ -152,7 +160,6 @@ namespace Barotrauma
}
else
{
targetItem.ParentInventory.RemoveItem(targetItem);
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
{
IsCompleted = true;
@@ -163,7 +170,6 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
targetItem.Drop(character);
}
}
}
@@ -172,22 +178,23 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear);
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear)
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
abortCondition = () => targetItem == null || targetItem.GetRootContainer() != rootContainer,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = moveToTarget.Name
};
},
onAbandon: () =>
{
targetItem = null;
moveToTarget = null;
ignoredItems.Add(targetItem);
RemoveSubObjective(ref goToObjective);
Reset();
},
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
/// <summary>
/// searches for an item that matches the desired item and adds a goto subobjective if one is found
/// </summary>
private void FindTargetItem()
{
if (itemIdentifiers == null)
@@ -205,16 +212,15 @@ namespace Barotrauma
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
if (ignoredItems.Contains(item)) { continue; }
if (item.Submarine == null) { continue; }
if (item.CurrentHull == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.CurrentHull == null || item.Condition <= 0.0f) { continue; }
if (itemIdentifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
@@ -228,19 +234,24 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
itemPriority *= distanceFactor;
itemPriority *= item.Condition / item.MaxCondition;
//ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootContainer ?? item;
this.rootContainer = rootContainer;
}
//if searched through all the items and a target wasn't found, can't be completed
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null)
if (currSearchIndex >= Item.ItemList.Count - 1)
{
isDoneSeeking = true;
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
#endif
Abandon = true;
Abandon = true;
}
}
}
@@ -253,7 +264,7 @@ namespace Barotrauma
}
else if (itemIdentifiers != null)
{
var matchingItem = character.Inventory.FindItem(i => !ignoredItems.Contains(i) && itemIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)), recursive: true);
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
{
return !equip || character.HasEquippedItem(matchingItem);
@@ -262,5 +273,24 @@ namespace Barotrauma
}
return false;
}
private bool CheckItem(Item item)
{
if (ignoredItems.Contains(item)) { return false; };
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
return itemIdentifiers.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
}
public override void Reset()
{
base.Reset();
RemoveSubObjective(ref goToObjective);
targetItem = null;
moveToTarget = null;
rootContainer = null;
isDoneSeeking = false;
currSearchIndex = 0;
}
}
}
@@ -15,8 +15,14 @@ namespace Barotrauma
private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded;
/// <summary>
/// Doesn't allow the objective to complete if this condition is false
/// </summary>
public Func<bool> requiredCondition;
public Func<PathNode, bool> startNodeFilter;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public bool followControlledCharacter;
@@ -38,19 +44,26 @@ namespace Barotrauma
public bool AllowGoingOutside { get; set; }
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public string DialogueIdentifier { get; set; }
public string TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
public override float GetPriority()
{
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)
if (followControlledCharacter && Character.Controlled == null)
{
return AIObjectiveManager.OrderPriority;
Priority = 0;
}
return 1.0f;
if (Target is Entity e && e.Removed)
{
Priority = 0;
}
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
{
Priority = 0;
}
return objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : Priority;
}
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
@@ -67,6 +80,21 @@ namespace Barotrauma
}
}
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true);
if (msg != null)
{
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
}
}
protected override void Act(float deltaTime)
{
if (followControlledCharacter)
@@ -74,6 +102,7 @@ namespace Barotrauma
if (Character.Controlled == null)
{
Abandon = true;
SteeringManager.Reset();
return;
}
Target = Character.Controlled;
@@ -94,13 +123,15 @@ namespace Barotrauma
if (e.Removed)
{
Abandon = true;
SteeringManager.Reset();
return;
}
else
{
character.AIController.SelectTarget(e.AiTarget);
}
}
var targetHull = Target is Hull h ? h : Target is Item i ? i.CurrentHull : Target is Character c ? c.CurrentHull : character.CurrentHull;
Hull targetHull = GetTargetHull();
if (!followControlledCharacter)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
@@ -110,8 +141,6 @@ namespace Barotrauma
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
{
Abandon = true;
SteeringManager.Reset();
return;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
@@ -127,6 +156,7 @@ namespace Barotrauma
{
if (repeat)
{
SpeakCannotReach();
SteeringManager.Reset();
}
else
@@ -137,13 +167,7 @@ namespace Barotrauma
}
if (Abandon)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && objectiveManager.CurrentOrder.ReportFailures)
{
character.Speak(TextManager.Get("DialogCannotReach"), identifier: "cannotreach", minDurationBetweenSimilar: 10.0f);
}
SpeakCannotReach();
SteeringManager.Reset();
}
else
@@ -194,23 +218,63 @@ namespace Barotrauma
{
nodeFilter = node => node.Waypoint.CurrentHull != null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, startNodeFilter, endNodeFilter, nodeFilter);
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, n =>
{
if (n.Waypoint.isObstructed) { return false; }
return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null);
}, endNodeFilter, nodeFilter);
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
}
if (!insideSteering && character.CurrentHull == null)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
private Hull GetTargetHull()
{
if (Target is Hull h)
{
return h;
}
else if (Target is Item i)
{
return i.CurrentHull;
}
else if (Target is Character c)
{
return c.CurrentHull;
}
else if (Target is Gap g)
{
return g.FlowTargetHull;
}
else if (Target is WayPoint wp)
{
return wp.CurrentHull;
}
else if (Target is FireSource fs)
{
return fs.Hull;
}
return null;
}
private bool IsCloseEnough
{
get
{
if (character.IsClimbing && SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished)
{
// Still in ladders and the path is not finished -> don't release
return false;
}
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (closeEnough)
{
@@ -231,6 +295,11 @@ namespace Barotrauma
Abandon = true;
return false;
}
if (abortCondition != null && abortCondition())
{
Abandon = true;
return false;
}
if (repeat)
{
return false;
@@ -10,6 +10,7 @@ namespace Barotrauma
class AIObjectiveIdle : AIObjective
{
public override string DebugTag => "idle";
public override bool UnequipItems => true;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
@@ -44,20 +45,17 @@ namespace Barotrauma
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public void SetRandom()
public void CalculatePriority()
{
Random = Rand.Range(0.5f, 1.5f);
randomTimer = randomUpdateInterval;
}
public override float GetPriority()
{
float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
float initiative = character.GetSkillLevel("initiative");
Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
return Priority;
}
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (objectiveManager.CurrentObjective == this)
@@ -68,7 +66,7 @@ namespace Barotrauma
}
else
{
SetRandom();
CalculatePriority();
}
}
}
@@ -103,9 +101,9 @@ namespace Barotrauma
{
newTargetTimer = 0;
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0)
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
{
// Don't allow new targets when climbing.
// Don't allow new targets when climbing straight up or down
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
}
}
@@ -181,7 +179,7 @@ namespace Barotrauma
if (!character.IsClimbing)
{
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
Wander(deltaTime);
return;
@@ -263,9 +261,9 @@ namespace Barotrauma
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName?.ToLowerInvariant();
string hullName = hull.RoomName;
if (hullName == null) { return false; }
return hullName.Contains("ballast") || hullName.Contains("airlock");
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -14,6 +14,9 @@ namespace Barotrauma
private float ignoreListTimer;
protected float targetUpdateTimer;
private float syncTimer;
private readonly float syncTime = 1;
// By default, doesn't clear the list automatically
protected virtual float IgnoreListClearInterval => 0;
@@ -42,7 +45,7 @@ namespace Barotrauma
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public override bool ReportFailures => false;
public virtual bool InverseTargetEvaluation => false;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
@@ -68,22 +71,24 @@ namespace Barotrauma
{
targetUpdateTimer -= deltaTime;
}
// Sync objectives, subobjectives and targets
foreach (var objective in Objectives)
if (syncTimer < 0)
{
var target = objective.Key;
//if (!objective.Value.CanBeCompleted && !ignoreList.Contains(target))
//{
// // TODO: leaks that cannot be accessed from inside cause FixLeak objective to fail, but for some reason it's not ignored. Make sure that it is.
// ignoreList.Add(target);
// targetUpdateTimer = 0;
//}
if (!Targets.Contains(target))
syncTimer = syncTime * Rand.Range(0.9f, 1.1f);
// Sync objectives, subobjectives and targets
foreach (var objective in Objectives)
{
subObjectives.Remove(objective.Value);
var target = objective.Key;
if (!Targets.Contains(target))
{
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
}
else
{
syncTimer -= deltaTime;
}
SyncRemovedObjectives(Objectives, GetList());
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
{
CreateObjectives();
@@ -103,21 +108,46 @@ namespace Barotrauma
public override float GetPriority()
{
if (character.LockHands) { return 0; }
if (character.Submarine == null) { return 0; }
if (Targets.None()) { return 0; }
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1) { return 0; }
if (objectiveManager.CurrentOrder == this)
if (character.LockHands || character.Submarine == null || Targets.None())
{
return AIObjectiveManager.OrderPriority;
Priority = 0;
}
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float devotion = MathHelper.Min(10, Priority);
float value = MathHelper.Clamp((devotion + targetValue * PriorityModifier) / 100, 0, 1);
return MathHelper.Lerp(0, max, value);
else
{
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
{
targetValue = 100 - targetValue;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
{
Priority = 0;
}
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
else
{
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
}
return Priority;
}
protected void UpdateTargets()
@@ -41,6 +41,15 @@ namespace Barotrauma
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
@@ -131,6 +140,8 @@ namespace Barotrauma
}
coroutine = CoroutineManager.InvokeAfter(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
@@ -144,7 +155,7 @@ namespace Barotrauma
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
if (CurrentOrder != null && firstObjective != null && CurrentOrder.GetPriority() > firstObjective.GetPriority())
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
{
CurrentObjective = CurrentOrder;
}
@@ -156,14 +167,14 @@ namespace Barotrauma
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().SetRandom();
GetObjective<AIObjectiveIdle>().CalculatePriority();
}
return CurrentObjective;
}
public float GetCurrentPriority()
{
return CurrentObjective == null ? 0.0f : CurrentObjective.GetPriority();
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
}
public void UpdateObjectives(float deltaTime)
@@ -203,7 +214,8 @@ namespace Barotrauma
{
if (Objectives.Any())
{
Objectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
Objectives.ForEach(o => o.GetPriority());
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
@@ -253,13 +265,14 @@ namespace Barotrauma
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
followControlledCharacter = orderGiver == character,
mimic = true
mimic = true,
DialogueIdentifier = "dialogcannotreachplace"
};
break;
case "wait":
newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = true
AllowGoingOutside = character.CurrentHull == null
};
break;
case "fixleaks":
@@ -294,7 +307,7 @@ namespace Barotrauma
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
default:
@@ -303,7 +316,7 @@ namespace Barotrauma
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
}
@@ -3,12 +3,14 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveOperateItem : AIObjective
{
public override string DebugTag => "operate item";
public override bool UnequipItems => true;
private ItemComponent component, controller;
private Entity operateTarget;
@@ -30,19 +32,32 @@ namespace Barotrauma
public override float GetPriority()
{
if (component.Item.ConditionPercentage <= 0) { return 0; }
if (objectiveManager.CurrentOrder == this)
if (component.Item.ConditionPercentage <= 0)
{
return AIObjectiveManager.OrderPriority;
Priority = 0;
}
if (component.Item.CurrentHull == null) { return 0; }
if (component.Item.CurrentHull.FireSources.Count > 0) { return 0; }
if (IsOperatedByAnother(GetTarget())) { return 0; }
if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float devotion = MathHelper.Min(10, Priority);
float value = devotion + AIObjectiveManager.OrderPriority * PriorityModifier;
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
return MathHelper.Clamp(value, 0, max);
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.None() || IsOperatedByAnother(GetTarget()))
{
Priority = 0;
}
else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
}
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
@@ -136,7 +151,11 @@ namespace Barotrauma
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50),
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
@@ -11,7 +11,7 @@ namespace Barotrauma
{
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool UnequipItems => true;
private IEnumerable<Pump> pumpList;
@@ -30,21 +30,28 @@ namespace Barotrauma
{
// TODO: priority list?
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; }
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
{
distanceFactor = 1;
Priority = 0;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = IsRepairing ? 50 : 0;
float devotion = (Math.Min(Priority, 10) + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + damagePriority * distanceFactor * successFactor * PriorityModifier, 0, 1));
else
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = IsRepairing ? 50 : 0;
float devotion = (CumulatedDevotion + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override bool Check()
@@ -135,7 +142,10 @@ namespace Barotrauma
{
if (character.SelectedConstruction != Item)
{
Item.TryInteract(character, true, true);
if (!Item.TryInteract(character, true, true))
{
Abandon = true;
}
}
if (previousCondition == -1)
{
@@ -151,11 +161,11 @@ namespace Barotrauma
{
if (IsRepairing)
{
character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
repairable.StopRepairing(character);
}
else
else if (repairable.CurrentFixer != character)
{
repairable.StartRepairing(character, Repairable.FixActions.Repair);
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
@@ -60,7 +61,11 @@ namespace Barotrauma
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (!Objectives.ContainsKey(item))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { return false; }
if (item != character.SelectedConstruction)
{
float condition = item.ConditionPercentage;
if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; }
}
}
if (RequireAdequateSkills)
{
@@ -69,7 +74,32 @@ namespace Barotrauma
return true;
}
protected override float TargetEvaluation() => Targets.Max(t => character.SelectedConstruction == t && t.ConditionPercentage < 100 ? 100 : 100 - t.ConditionPercentage);
protected override float TargetEvaluation()
{
if (character.SelectedConstruction != null && Targets.Any(t => character.SelectedConstruction == t && t.ConditionPercentage < 100))
{
// Don't stop fixing until done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>(), onlyBots: true);
int items = Targets.Count;
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
else
{
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
}
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
@@ -14,7 +14,7 @@ namespace Barotrauma
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 150.0f;
const float CloseEnoughToTreat = 100.0f;
private readonly Character targetCharacter;
@@ -44,6 +44,15 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character)
{
var otherCharacter = character.SelectedBy;
if (otherCharacter != null)
{
// Someone else is rescuing/holding the target.
Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical");
}
}
if (targetCharacter != character)
{
@@ -60,9 +69,18 @@ namespace Barotrauma
if (!character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat },
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
@@ -81,7 +99,11 @@ namespace Barotrauma
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
}
}
}
@@ -93,9 +115,18 @@ namespace Barotrauma
{
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat },
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
@@ -117,6 +148,11 @@ namespace Barotrauma
private Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private void GiveTreatment(float deltaTime)
{
if (!targetCharacter.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController.SteeringManager.Reset();
}
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
@@ -127,9 +163,8 @@ namespace Barotrauma
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
var allAfflictions = GetVitalityReducingAfflictions(targetCharacter).OrderByDescending(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth));
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in allAfflictions)
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
@@ -190,10 +225,12 @@ namespace Barotrauma
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
}
character.AnimController.Anim = AnimController.Animation.CPR;
if (character != targetCharacter)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
@@ -202,7 +239,7 @@ namespace Barotrauma
{
if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; }
#if CLIENT
ic.PlaySound(ActionType.OnUse, character.WorldPosition, character);
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
@@ -230,7 +267,7 @@ namespace Barotrauma
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager);
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
@@ -243,20 +280,24 @@ namespace Barotrauma
{
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
return 0;
Priority = 0;
}
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
else
{
distanceFactor = 1;
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
}
float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1));
return Priority;
}
public static IEnumerable<Affliction> GetVitalityReducingAfflictions(Character character) => character.CharacterHealth.GetAllAfflictions(a => a.GetVitalityDecrease(character.CharacterHealth) > 0);
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
}
}
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
{
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 100;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
{
if (manager == null)
{
return vitalityThreshold;
}
else
{
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
}
}
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
{
int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRescueAll>(), onlyBots: true);
int targetCount = Targets.Count;
bool anyRescuers = otherRescuers > 0;
float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Min(t => GetVitalityFactor(t)) / ratio;
}
else
{
float multiplier = 1;
if (anyRescuers)
{
float mySkill = character.GetSkillLevel("medical");
int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true);
if (targetCount / (float)betterRescuers <= 1)
{
// Enough rescuers
return 100;
}
else
{
bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor");
if (foundOtherMedics)
{
if (character.Info.Job.Prefab.Identifier != "medicaldoctor")
{
// Double the vitality factor -> less likely to take action
multiplier = 2;
}
}
}
}
return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier;
}
}
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
return Math.Clamp(vitality, 0, 100);
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
{
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
}
else
{
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
return false;
}
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
}
}
@@ -2,15 +2,25 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma
{
public enum OrderCategory
{
Emergency,
Movement,
Power,
Maintenance,
Operate,
Undefined
}
class Order
{
public static Dictionary<string, Order> Prefabs { get; private set; }
public static Dictionary<OrderCategory, Tuple<Sprite, Color>> OrderCategoryIcons { get; private set; }
public static List<Order> PrefabList { get; private set; }
public static Order GetPrefab(string identifier)
{
@@ -36,7 +46,30 @@ namespace Barotrauma
public readonly string Identifier;
public readonly Color Color;
private Color? color;
public Color Color
{
get
{
if (color.HasValue)
{
return color.Value;
}
else if (OrderCategoryIcons.TryGetValue(Category, out Tuple<Sprite, Color> sprite))
{
return sprite.Item2;
}
else
{
return Color.White;
}
}
private set
{
color = value;
}
}
//if true, the order is issued to all available characters
public bool TargetAllCharacters;
@@ -49,19 +82,26 @@ namespace Barotrauma
public Controller ConnectedController;
public Character OrderGiver;
public readonly OrderCategory Category;
//legacy support
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
public readonly Dictionary<string, Sprite> OptionSprites;
public readonly float Weight;
static Order()
{
Prefabs = new Dictionary<string, Order>();
OrderCategoryIcons = new Dictionary<OrderCategory, Tuple<Sprite, Color>>();
foreach (string file in GameMain.Instance.GetFilesOfType(ContentType.Orders))
foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.Orders))
{
XDocument doc = XMLExtensions.TryLoadXml(file);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
bool allowOverriding = false;
@@ -72,38 +112,65 @@ namespace Barotrauma
}
foreach (XElement sourceElement in mainElement.Elements())
{
var orderElement = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = orderElement.Name.ToString();
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = element.Name.ToString();
if (name.Equals("order", StringComparison.OrdinalIgnoreCase))
{
string identifier = orderElement.GetAttributeString("identifier", null);
string identifier = element.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"Error in file {file}: The order element '{name}' does not have an identifier! All orders must have a unique identifier.");
DebugConsole.ThrowError($"Error in file {file.Path}: The order element '{name}' does not have an identifier! All orders must have a unique identifier.");
continue;
}
if (Prefabs.TryGetValue(identifier, out Order duplicate))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding an existing order '{identifier}' with another one defined in '{file}'", Color.Yellow);
DebugConsole.NewMessage($"Overriding an existing order '{identifier}' with another one defined in '{file.Path}'", Color.Yellow);
Prefabs.Remove(identifier);
}
else
{
DebugConsole.ThrowError($"Error in file {file}: Duplicate element with the idenfitier '{identifier}' found in '{file}'! All orders must have a unique identifier. Use <override></override> tags to override an order with the same identifier.");
DebugConsole.ThrowError($"Error in file {file.Path}: Duplicate element with the idenfitier '{identifier}' found in '{file.Path}'! All orders must have a unique identifier. Use <override></override> tags to override an order with the same identifier.");
continue;
}
}
var newOrder = new Order(orderElement);
var newOrder = new Order(element);
newOrder.Prefab = newOrder;
Prefabs.Add(identifier, newOrder);
}
else if (name.Equals("ordercategory", StringComparison.OrdinalIgnoreCase))
{
var category = (OrderCategory)Enum.Parse(typeof(OrderCategory), element.GetAttributeString("category", "undefined"), true);
if (OrderCategoryIcons.ContainsKey(category))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding an existing icon for the '{category}' order category with another one defined in '{file}'", Color.Yellow);
OrderCategoryIcons.Remove(category);
}
else
{
DebugConsole.ThrowError($"Error in file {file}: Duplicate element for the '{category}' order category found in '{file}'! All order categories must be unique. Use <override></override> tags to override an order category.");
continue;
}
}
var spriteElement = element.GetChildElement("sprite");
if (spriteElement != null)
{
var sprite = new Sprite(spriteElement, lazyLoad: true);
var color = element.GetAttributeColor("color", Color.White);
OrderCategoryIcons.Add(category, new Tuple<Sprite, Color>(sprite, color));
}
}
}
}
PrefabList = new List<Order>(Prefabs.Values);
}
/// <summary>
/// Constructor for order prefabs
/// </summary>
private Order(XElement orderElement)
{
Identifier = orderElement.GetAttributeString("identifier", "");
@@ -124,12 +191,14 @@ namespace Barotrauma
}
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
Color = orderElement.GetAttributeColor("color", Color.White);
color = orderElement.GetAttributeColor("color");
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
UseController = orderElement.GetAttributeBool("usecontroller", false);
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), orderElement.GetAttributeString("category", "undefined"), true);
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true);
if (translatedOptionNames == null)
@@ -152,17 +221,31 @@ namespace Barotrauma
OptionNames = Options;
}
foreach (XElement subElement in orderElement.Elements())
var spriteElement = orderElement.GetChildElement("sprite");
if (spriteElement != null)
{
switch (subElement.Name.ToString().ToLowerInvariant())
SymbolSprite = new Sprite(spriteElement, lazyLoad: true);
}
OptionSprites = new Dictionary<string, Sprite>();
if (Options != null && Options.Length > 0)
{
var optionSpriteElements = orderElement.GetChildElement("optionsprites")?.GetChildElements("sprite");
if (optionSpriteElements != null && optionSpriteElements.Any())
{
case "sprite":
SymbolSprite = new Sprite(subElement, lazyLoad: true);
break;
for (int i = 0; i < Options.Length; i++)
{
if (i >= optionSpriteElements.Count()) { break; };
var sprite = new Sprite(optionSpriteElements.ElementAt(i), lazyLoad: true);
OptionSprites.Add(Options[i], sprite);
}
}
}
}
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
{
Prefab = prefab;
@@ -177,6 +260,8 @@ namespace Barotrauma
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
Weight = prefab.Weight;
Category = prefab.Category;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
@@ -199,32 +284,31 @@ namespace Barotrauma
if (character.Info == null || character.Info.Job == null) { return false; }
if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; }
if (!JobPrefab.List.Values.Any(jp => jp.AppropriateOrders.Contains(Identifier)) &&
if (!JobPrefab.Prefabs.Any(jp => jp.AppropriateOrders.Contains(Identifier)) &&
(AppropriateJobs == null || AppropriateJobs.Length == 0))
{
return true;
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) return true;
if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "")
{
orderOption = orderOption ?? "";
orderOption ??= "";
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier;
if (!string.IsNullOrEmpty(orderOption)) messageTag += "." + orderOption;
if (!string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (targetCharacterName == null) targetCharacterName = "";
if (targetRoomName == null) targetRoomName = "";
if (targetCharacterName == null) { targetCharacterName = ""; }
if (targetRoomName == null) { targetRoomName = ""; }
string msg = TextManager.GetWithVariables(messageTag, new string[2] { "[name]", "[roomname]" }, new string[2] { targetCharacterName, targetRoomName }, new bool[2] { false, true }, true);
if (msg == null) return "";
if (msg == null) { return ""; }
return msg;
}
}
}
@@ -91,18 +91,18 @@ namespace Barotrauma
private List<PathNode> nodes;
private bool insideSubmarine;
public bool InsideSubmarine { get; set; }
public PathFinder(List<WayPoint> wayPoints, bool insideSubmarine = false)
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false)
{
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => (w.Submarine != null) == insideSubmarine));
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering));
foreach (WayPoint wp in wayPoints)
{
wp.linkedTo.CollectionChanged += WaypointLinksChanged;
}
this.insideSubmarine = insideSubmarine;
InsideSubmarine = indoorsSteering;
}
void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
@@ -180,16 +180,19 @@ namespace Barotrauma
yDiff += 10.0f;
}
float dist = xDiff + (insideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
float dist = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
//prefer nodes that are closer to the end position
dist += (Math.Abs(end.X - nodePos.X) + Math.Abs(end.Y - nodePos.Y)) / 2.0f;
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && insideSubmarine) dist *= 10.0f;
if (node.Waypoint.CurrentHull == null && InsideSubmarine)
{
dist *= 10.0f;
}
if (dist < closestDist || startNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (insideSubmarine)
if (InsideSubmarine)
{
var body = Submarine.PickBody(
start, nodePos, null,
@@ -230,7 +233,7 @@ namespace Barotrauma
nodePos -= diff;
}
float dist = Vector2.DistanceSquared(end, nodePos);
if (insideSubmarine)
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) dist *= 10.0f;
@@ -240,7 +243,7 @@ namespace Barotrauma
if (dist < closestDist || endNode == null)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (insideSubmarine)
if (InsideSubmarine)
{
var body = Submarine.PickBody(end, nodePos, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
@@ -3,6 +3,8 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -143,8 +145,8 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Collider.Enabled = false;
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
Collider.LinearVelocity = mainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
@@ -192,7 +194,7 @@ namespace Barotrauma
Collider.FarseerBody.FixedRotation = false;
UpdateSineAnim(deltaTime);
}
else if (CanEnterSubmarine && (currentHull != null || forceStanding))
else if (RagdollParams.CanWalk && (currentHull != null || forceStanding))
{
if (CurrentGroundedParams != null)
{
@@ -229,8 +231,8 @@ namespace Barotrauma
}
else
{
float refAngle = 0.0f;
Limb refLimb = GetLimb(LimbType.Head);
float refAngle;
if (refLimb == null)
{
refAngle = CurrentAnimationParams.TorsoAngleInRadians;
@@ -259,7 +261,10 @@ namespace Barotrauma
}
}
if (character.SelectedCharacter != null) DragCharacter(character.SelectedCharacter, deltaTime);
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
}
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
@@ -321,24 +326,25 @@ namespace Barotrauma
}
}
Character targetCharacter = target;
float eatSpeed = character.Mass / targetCharacter.Mass * 0.1f;
float dmg = character.Params.EatingSpeed;
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
eatTimer += deltaTime * eatSpeed;
Vector2 mouthPos = GetMouthPosition().Value;
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
float limbDist = limbDiff.Length();
if (limbDist < 1.0f)
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1);
if (limbDiff.LengthSquared() < extent * extent)
{
//pull the target character to the position of the mouth
//(+ make the force fluctuate to waggle the character a bit)
if (CanDrag(target))
float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40);
if (dragForce > 0.1f)
{
targetCharacter.AnimController.MainLimb.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + 10.0f));
targetCharacter.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation, 20.0f);
targetCharacter.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + 10.0f));
target.AnimController.MainLimb.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce));
target.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation, dragForce * 2);
target.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce));
}
//pull the character's mouth to the target character (again with a fluctuating force)
@@ -347,23 +353,29 @@ namespace Barotrauma
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f);
if (Rand.Value() < particleFrequency / 6)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false);
}
if (Rand.Value() < particleFrequency)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false);
}
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
{
//apply damage to the target character to get some blood particles flying
targetCharacter.AnimController.MainLimb.AddDamage(targetCharacter.SimPosition, 0.0f, 20.0f, 0.0f, false);
bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered;
//keep severing joints until there is only one limb left
LimbJoint[] nonSeveredJoints = Array.FindAll(targetCharacter.AnimController.LimbJoints,
l => !l.IsSevered && l.CanBeSevered && l.LimbA != null && !l.LimbA.IsSevered && l.LimbB != null && !l.LimbB.IsSevered);
if (nonSeveredJoints.Length == 0)
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
if (nonSeveredJoints.None())
{
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(targetCharacter);
Entity.Spawner?.AddToRemoveQueue(target);
character.SelectedCharacter = null;
}
else //sever a random joint
{
targetCharacter.AnimController.SeverLimbJoint(nonSeveredJoints[Rand.Int(nonSeveredJoints.Length)]);
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom());
}
}
}
@@ -382,7 +394,13 @@ namespace Barotrauma
if (movement.LengthSquared() > 0.00001f)
{
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, 0.5f);
float t = 0.5f;
if (CurrentSwimParams.RotateTowardsMovement && VectorExtensions.Angle(VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2), movement) > MathHelper.PiOver2)
{
// Reduce the linear movement speed when not facing the movement direction
t /= 5;
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
//limbs are disabled when simple physics is enabled, no need to move them
@@ -694,9 +712,14 @@ namespace Barotrauma
(-transformedStepSize.Y > 0.0f) ? -transformedStepSize.Y : 0.0f);
limb.MoveToPos(limb.DebugTargetPos, FootMoveForce);
}
if (playFootstepSound)
{
#if CLIENT
if (playFootstepSound) { PlayImpactSound(limb); }
PlayImpactSound(limb);
#endif
}
if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb,
@@ -735,6 +758,10 @@ namespace Barotrauma
if (limb.LightSource != null)
{
limb.LightSource.Color = Color.Lerp(limb.InitialLightSourceColor, Color.TransparentBlack, deathAnimTimer / deathAnimDuration);
if (limb.InitialLightSpriteAlpha.HasValue)
{
limb.LightSource.OverrideLightSpriteAlpha = MathHelper.Lerp(limb.InitialLightSpriteAlpha.Value, 0.0f, deathAnimTimer / deathAnimDuration);
}
}
#endif
if (limb.type == LimbType.Head || limb.type == LimbType.Tail || limb.IsSevered || !limb.body.Enabled) continue;
@@ -151,7 +151,7 @@ namespace Barotrauma
private bool aiming;
private float movementLerp;
private readonly float movementLerp;
private float cprAnimTimer;
private float cprPump;
@@ -239,6 +239,7 @@ namespace Barotrauma
//calculate arm and forearm length (atm this assumes that both arms are the same size)
Limb rightForearm = GetLimb(LimbType.RightForearm);
Limb rightHand = GetLimb(LimbType.RightHand);
if (rightHand == null) { return; }
shoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm);
Vector2 localAnchorShoulder = Vector2.Zero;
@@ -263,12 +264,14 @@ namespace Barotrauma
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
}
else if (rightForearm != null && rightHand != null)
else
{
LimbJoint rightWrist = rightForearm == null ? null : GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand);
LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand);
forearmLength = Vector2.Distance(
rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB,
rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
forearmLength += Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
@@ -596,6 +599,7 @@ namespace Barotrauma
movement.Y = 0.0f;
if (head == null) { return; }
if (torso == null) { return; }
bool isNotRemote = true;
@@ -620,8 +624,7 @@ namespace Barotrauma
waist.PullJointEnabled = true;
}
float floorPos = GetFloorY(colliderPos + new Vector2(Math.Sign(movement.X) * 0.5f, 1.0f), ignoreStairs: Stairs == null);
bool onSlope = floorPos > GetColliderBottom().Y + 0.05f;
bool onSlope = Math.Abs(movement.X) > 0.01f && Math.Abs(floorNormal.X) > 0.1f && Math.Sign(floorNormal.X) != Math.Sign(movement.X);
if (Stairs != null || onSlope)
{
@@ -704,6 +707,7 @@ namespace Barotrauma
for (int i = -1; i < 2; i += 2)
{
Limb foot = i == -1 ? leftFoot : rightFoot;
if (foot == null) { continue; }
Vector2 footPos = stepSize * -i;
footPos += new Vector2(Math.Sign(movement.X) * FootMoveOffset.X, FootMoveOffset.Y);
@@ -749,7 +753,7 @@ namespace Barotrauma
Vector2 posAddition = new Vector2(Math.Sign(movement.X) * HandMoveOffset.X, HandMoveOffset.Y);
if (!rightHand.Disabled)
if (rightHand != null && !rightHand.Disabled)
{
HandIK(rightHand, torso.SimPosition + posAddition +
new Vector2(
@@ -757,7 +761,7 @@ namespace Barotrauma
(Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), CurrentGroundedParams.HandMoveStrength);
}
if (!leftHand.Disabled)
if (leftHand != null && !leftHand.Disabled)
{
HandIK(leftHand, torso.SimPosition + posAddition +
new Vector2(
@@ -785,12 +789,12 @@ namespace Barotrauma
if (Stairs == null)
{
footPos.Y = Math.Max(Math.Min(floorPos, footPos.Y + 0.5f), footPos.Y);
footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y);
}
var foot = i == -1 ? rightFoot : leftFoot;
if (!foot.Disabled)
if (foot != null && !foot.Disabled)
{
foot.DebugRefPos = colliderPos;
foot.DebugTargetPos = footPos;
@@ -803,14 +807,14 @@ namespace Barotrauma
for (int i = 0; i < 2; i++)
{
var hand = i == 0 ? rightHand : leftHand;
if (hand.Disabled) { continue; }
if (hand == null || hand.Disabled) { continue; }
var armType = i == 0 ? LimbType.RightArm : LimbType.LeftArm;
var foreArmType = i == 0 ? LimbType.RightForearm : LimbType.LeftForearm;
//get the upper arm to point downwards
var arm = GetLimb(armType);
if (Math.Abs(arm.body.AngularVelocity) < 10.0f)
if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f)
{
arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.1f, 0.1f), arm.Mass * 10.0f);
}
@@ -861,6 +865,8 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
if (head == null) { return; }
if (torso == null) { return; }
if (currentHull != null)
{
@@ -999,12 +1005,19 @@ namespace Barotrauma
legCyclePos += Vector2.Normalize(movement).Length();
handCyclePos += MathHelper.ToRadians(CurrentSwimParams.HandCycleSpeed) * Math.Sign(movement.X);
footPos = GetLimb(LimbType.Waist).SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength);
var waist = GetLimb(LimbType.Waist);
footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount * CurrentAnimationParams.CycleSpeed, 0.0f);
transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation));
FootIK(rightFoot, footPos - transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
FootIK(leftFoot, footPos + transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
if (rightFoot != null && !rightFoot.Disabled)
{
FootIK(rightFoot, footPos - transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
}
if (leftFoot != null && !leftFoot.Disabled)
{
FootIK(leftFoot, footPos + transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
}
handPos = (torso.SimPosition + head.SimPosition) / 2.0f;
@@ -1015,14 +1028,14 @@ namespace Barotrauma
float wobbleAmount = 0.1f;
if (!rightHand.Disabled)
if (rightHand != null && !rightHand.Disabled)
{
MoveLimb(rightHand, new Vector2(
handPos.X + (float)Math.Sin(handCyclePos / 1.5f) * wobbleAmount,
handPos.Y + (float)Math.Sin(handCyclePos / 3.5f) * wobbleAmount - 0.25f), 1.5f);
}
if (!leftHand.Disabled)
if (leftHand != null && !leftHand.Disabled)
{
MoveLimb(leftHand, new Vector2(
handPos.X + (float)Math.Sin(handCyclePos / 2.0f) * wobbleAmount,
@@ -1042,7 +1055,7 @@ namespace Barotrauma
Matrix rotationMatrix = Matrix.CreateRotationZ(torso.Rotation);
if (!rightHand.Disabled)
if (rightHand != null && !rightHand.Disabled)
{
Vector2 rightHandPos = new Vector2(-handPosX, -handPosY) + handMoveOffset;
rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X);
@@ -1051,7 +1064,7 @@ namespace Barotrauma
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength);
}
if (!leftHand.Disabled)
if (leftHand != null && !leftHand.Disabled)
{
Vector2 leftHandPos = new Vector2(handPosX, handPosY) + handMoveOffset;
leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X);
@@ -1081,18 +1094,20 @@ namespace Barotrauma
movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f);
Limb leftFoot = GetLimb(LimbType.LeftFoot);
Limb rightFoot = GetLimb(LimbType.RightFoot);
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
Limb leftFoot = GetLimb(LimbType.LeftFoot);
Limb rightFoot = GetLimb(LimbType.RightFoot);
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
character.SelectedConstruction.Rect.X + character.SelectedConstruction.Rect.Width / 2.0f,
character.SelectedConstruction.Rect.Y);
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Size.ToVector2());
float stepHeight = ConvertUnits.ToSimUnits(30.0f);
if (currentHull == null && character.SelectedConstruction.Submarine != null)
@@ -1106,10 +1121,10 @@ namespace Barotrauma
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f;
MoveLimb(head, new Vector2(ladderSimPos.X - 0.27f * Dir, bottomPos + WalkParams.HeadPosition), 10.5f);
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.27f * Dir, bottomPos + WalkParams.TorsoPosition), 10.5f);
MoveLimb(head, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + WalkParams.HeadPosition), 10.5f);
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + WalkParams.TorsoPosition), 10.5f);
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.2f * Dir, Collider.SimPosition.Y), 10.5f);
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f);
Vector2 handPos = new Vector2(
ladderSimPos.X,
@@ -1121,12 +1136,12 @@ namespace Barotrauma
if (!character.IsKeyDown(InputType.Aim) || Math.Abs(movement.Y) > 0.01f)
{
MoveLimb(leftHand,
new Vector2(handPos.X,
new Vector2(handPos.X - ladderSimSize.X * 0.5f,
(slide ? handPos.Y : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y),
5.2f);
5.2f); ;
MoveLimb(rightHand,
new Vector2(handPos.X,
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X,
(slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y),
5.2f);
@@ -1140,11 +1155,11 @@ namespace Barotrauma
//only move the feet if they're above the bottom of the ladders
//(if not, they'll just dangle in air, and the character holds itself up with it's arms)
if (footPos.Y > -ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Height))
if (footPos.Y > -ladderSimSize.Y)
{
if (slide)
{
MoveLimb(leftFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true);
MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true);
MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true);
}
else
@@ -1362,11 +1377,7 @@ namespace Barotrauma
target.LastDamageSource = null;
target.DamageLimb(
targetTorso.WorldPosition, targetTorso,
new List<Affliction>()
{
AfflictionPrefab.InternalDamage.Instantiate((CPRSettings.DamageSkillThreshold - skill) * CPRSettings.DamageSkillMultiplier,
source: character)
},
new[] { CPRSettings.InsufficientSkillAffliction.Instantiate((CPRSettings.DamageSkillThreshold - skill) * CPRSettings.DamageSkillMultiplier, source: character) },
0.0f, true, 0.0f, attacker: null);
}
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
@@ -1396,7 +1407,7 @@ namespace Barotrauma
target.CharacterHealth.CalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info.IncreaseSkillLevel("medical", 0.5f, character.WorldPosition + Vector2.UnitY * 150.0f);
character.Info.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.WorldPosition + Vector2.UnitY * 150.0f);
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
@@ -1764,11 +1775,6 @@ namespace Barotrauma
holdable.Pusher.Enabled = true;
holdable.Pusher.ResetDynamics();
holdable.Pusher.SetTransform(currItemPos, itemAngle);
foreach (Character character in Character.CharacterList)
{
holdable.Pusher.FarseerBody.RestoreCollisionWith(character.AnimController.Collider.FarseerBody);
}
holdable.Pusher.FarseerBody.IgnoreCollisionWith(Collider.FarseerBody);
}
else
{
@@ -1783,7 +1789,7 @@ namespace Barotrauma
}
}
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir);
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
if (!isClimbing)
{
@@ -1844,6 +1850,16 @@ namespace Barotrauma
private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle)
{
if (!MathUtils.IsValid(pos))
{
string errorMsg = "Invalid foot position in FootIK (" + pos + ")\n" + Environment.StackTrace;
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidPos", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
Limb upperLeg, lowerLeg;
if (foot.type == LimbType.LeftFoot)
{
@@ -1870,8 +1886,10 @@ namespace Barotrauma
float legAngle = MathUtils.VectorToAngle(pos - waistPos) + MathHelper.PiOver2;
if (!MathUtils.IsValid(legAngle))
{
string errorMsg = "Invalid leg angle (" + legAngle + ") in FootIK. Waist pos: " + waistPos + ", target pos: " + pos;
string errorMsg = "Invalid leg angle (" + legAngle + ") in FootIK. Waist pos: " + waistPos + ", target pos: " + pos + "\n" + Environment.StackTrace;
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidAngle", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
@@ -1952,9 +1970,9 @@ namespace Barotrauma
}
if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed)
{
character.SelectedItems[i].body.SetTransform(
/*character.SelectedItems[i].body.SetTransform(
character.SelectedItems[i].body.SimPosition,
MathUtils.WrapAngleTwoPi(character.SelectedItems[i].body.Rotation + MathHelper.Pi));
MathUtils.WrapAngleTwoPi(character.SelectedItems[i].body.Rotation + MathHelper.Pi));*/
character.SelectedItems[i].GetComponent<Holdable>()?.Flip();
}
}
@@ -3,7 +3,6 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using FarseerPhysics.Factories;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -25,7 +24,27 @@ namespace Barotrauma
/// </summary>
const float MaxImpactDamage = 0.1f;
private static List<Ragdoll> list = new List<Ragdoll>();
private static readonly List<Ragdoll> list = new List<Ragdoll>();
struct Impact
{
public Fixture F1, F2;
public Vector2 LocalNormal;
public Vector2 Velocity;
public Vector2 ImpactPos;
public Impact(Fixture f1, Fixture f2, Contact contact, Vector2 velocity)
{
F1 = f1;
F2 = f2;
Velocity = velocity;
LocalNormal = contact.Manifold.LocalNormal;
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
ImpactPos = points[0];
}
}
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
protected Hull currentHull;
@@ -59,8 +78,12 @@ namespace Barotrauma
if (frozen == value) return;
frozen = value;
Collider.PhysEnabled = !frozen;
Collider.FarseerBody.LinearDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f;
Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f;
Collider.FarseerBody.IgnoreGravity = frozen;
//Collider.PhysEnabled = !frozen;
if (frozen && MainLimb != null) MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition;
}
}
@@ -87,12 +110,15 @@ namespace Barotrauma
//a Character to the position sent by server in multiplayer mode
protected Vector2 overrideTargetMovement;
protected float floorY;
protected float floorY, standOnFloorY;
protected Vector2 floorNormal = Vector2.UnitY;
protected float surfaceY;
protected bool inWater, headInWater;
public bool onGround;
private bool ignorePlatforms;
private Vector2 lastFloorCheckPos;
private bool lastFloorCheckIgnoreStairs, lastFloorCheckIgnorePlatforms;
/// <summary>
/// In sim units. Joint scale applied.
@@ -110,8 +136,6 @@ namespace Barotrauma
private Category prevCollisionCategory = Category.None;
private Body outsideCollisionBlocker;
public bool IsStuck => Limbs.Any(l => l.IsStuck);
public PhysicsBody Collider
@@ -170,6 +194,14 @@ namespace Barotrauma
private set;
}
public void SubtractMass(Limb limb)
{
if (limbs.Contains(limb))
{
Mass -= limb.Mass;
}
}
public Limb MainLimb
{
get
@@ -263,6 +295,7 @@ namespace Barotrauma
public bool Draggable => RagdollParams.Draggable;
public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure));
public bool CanAttackCharacters => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Character));
public float Dir => dir == Direction.Left ? -1.0f : 1.0f;
@@ -295,15 +328,8 @@ namespace Barotrauma
}
}
public bool IgnorePlatforms
{
get { return ignorePlatforms; }
set
{
ignorePlatforms = value;
}
}
public bool IgnorePlatforms { get; set; }
/// <summary>
/// Call this to create the ragdoll from the RagdollParams.
/// </summary>
@@ -359,9 +385,10 @@ namespace Barotrauma
if (character.IsHusk)
{
if (Character.TryGetConfigFile(character.ConfigPath, out XDocument configFile))
var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath);
if (characterPrefab?.XDocument != null)
{
var mainElement = configFile.Root.IsOverride() ? configFile.Root.FirstElement() : configFile.Root;
var mainElement = characterPrefab.XDocument.Root.IsOverride() ? characterPrefab.XDocument.Root.FirstElement() : characterPrefab.XDocument.Root;
foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
{
AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this);
@@ -407,7 +434,10 @@ namespace Barotrauma
{
if (LimbJoints != null)
{
LimbJoints.ForEach(j => GameMain.World.RemoveJoint(j));
foreach (LimbJoint joint in LimbJoints)
{
if (GameMain.World.JointList.Contains(joint)) { GameMain.World.Remove(joint); }
}
}
DebugConsole.Log($"Creating joints from {RagdollParams.Name}.");
LimbJoints = new LimbJoint[RagdollParams.Joints.Count];
@@ -421,15 +451,7 @@ namespace Barotrauma
}
}
// This block was salvaged from the merge of the dev branch to the animation branch. Not sure if every line made it here.
outsideCollisionBlocker = BodyFactory.CreateEdge(GameMain.World, -Vector2.UnitX * 2.0f, Vector2.UnitX * 2.0f, "blocker");
outsideCollisionBlocker.BodyType = BodyType.Static;
outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall;
outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter;
outsideCollisionBlocker.Enabled = false;
UpdateCollisionCategories();
SetInitialLimbPositions();
}
@@ -499,7 +521,7 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
GameMain.World.AddJoint(joint);
GameMain.World.Add(joint);
for (int i = 0; i < LimbJoints.Length; i++)
{
if (LimbJoints[i] != null) continue;
@@ -578,63 +600,71 @@ namespace Barotrauma
LimbJoints = newJoints;
}
SubtractMass(limb);
limb.Remove();
foreach (LimbJoint limbJoint in attachedJoints)
{
GameMain.World.RemoveJoint(limbJoint);
GameMain.World.Remove(limbJoint);
}
}
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) return false;
//only collide with the ragdoll's own blocker
if (f2.Body.UserData as string == "blocker" && f2.Body != outsideCollisionBlocker) return false;
//using the velocity of the limb would make the impact damage more realistic,
//but would also make it harder to edit the animations because the forces/torques
//would all have to be balanced in a way that prevents the character from doing
//impact damage to itself
Vector2 velocity = Collider.LinearVelocity;
if (character.Submarine == null && f2.Body.UserData is Submarine)
{
velocity -= ((Submarine)f2.Body.UserData).Velocity;
}
//always collides with bodies other than structures
if (!(f2.Body.UserData is Structure structure))
{
CalculateImpact(f1, f2, contact);
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
return true;
}
Vector2 colliderBottom = GetColliderBottom();
if (structure.IsPlatform)
{
if (ignorePlatforms) return false;
if (IgnorePlatforms) { return false; }
//the collision is ignored if the lowest limb is under the platform
//if (lowestLimb==null || lowestLimb.Position.Y < structure.Rect.Y) return false;
if (colliderBottom.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) return false;
if (f1.Body.Position.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) return false;
if (colliderBottom.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; }
if (f1.Body.Position.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; }
}
else if (structure.StairDirection != Direction.None)
{
Stairs = null;
//don't collider with stairs if
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) return false;
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return false; }
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) return false;
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return false; }
//3. collided with the stairs from below
if (contact.Manifold.LocalNormal.Y < 0.0f) return false;
if (contact.Manifold.LocalNormal.Y < 0.0f) { return false; }
//4. contact points is above the bottom half of the collider
contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) return false;
if (points[0].Y > Collider.SimPosition.Y) { return false; }
//5. in water
if (inWater && targetMovement.Y < 0.5f) return false;
if (inWater && targetMovement.Y < 0.5f) { return false; }
//---------------
@@ -648,27 +678,19 @@ namespace Barotrauma
return false;
}
CalculateImpact(f1, f2, contact);
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
return true;
}
private void CalculateImpact(Fixture f1, Fixture f2, Contact contact)
private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity)
{
if (character.DisableImpactDamageTimer > 0.0f) return;
//using the velocity of the limb would make the impact damage more realistic,
//but would also make it harder to edit the animations because the forces/torques
//would all have to be balanced in a way that prevents the character from doing
//impact damage to itself
Vector2 velocity = Collider.LinearVelocity;
Vector2 normal = contact.Manifold.LocalNormal;
if (character.Submarine == null && f2.Body.UserData is Submarine)
{
velocity -= ((Submarine)f2.Body.UserData).Velocity;
}
Vector2 normal = localNormal;
float impact = Vector2.Dot(velocity, -normal);
if (f1.Body == Collider.FarseerBody || !Collider.Enabled)
{
@@ -679,8 +701,7 @@ namespace Barotrauma
{
if (impact > ImpactTolerance)
{
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
Vector2 impactPos = ConvertUnits.ToDisplayUnits(points[0]);
impactPos = ConvertUnits.ToDisplayUnits(impactPos);
if (character.Submarine != null) impactPos += character.Submarine.Position;
float impactDamage = Math.Min((impact - ImpactTolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage);
@@ -711,26 +732,32 @@ namespace Barotrauma
limbJoint.IsSevered = true;
limbJoint.Enabled = false;
Vector2 limbDiff = limbJoint.LimbA.SimPosition - limbJoint.LimbB.SimPosition;
if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); }
limbDiff = Vector2.Normalize(limbDiff);
float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass;
limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
List<Limb> connectedLimbs = new List<Limb>();
List<LimbJoint> checkedJoints = new List<LimbJoint>();
GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb);
foreach (Limb limb in Limbs)
{
if (connectedLimbs.Contains(limb)) continue;
limb.IsSevered = true;
if (connectedLimbs.Contains(limb)) { continue; }
limb.IsSevered = true;
}
SeverLimbJointProjSpecific(limbJoint);
SeverLimbJointProjSpecific(limbJoint, playSound: true);
if (GameMain.NetworkMember!=null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status });
}
}
partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound = true);
partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound);
private void GetConnectedLimbs(List<Limb> connectedLimbs, List<LimbJoint> checkedJoints, Limb limb)
{
@@ -894,7 +921,9 @@ namespace Barotrauma
//in -> out
if (newHull == null && currentHull.Submarine != null)
{
if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) return;
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) { return; }
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; }
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
}
@@ -922,11 +951,7 @@ namespace Barotrauma
private void PreventOutsideCollision()
{
if (currentHull?.Submarine == null)
{
outsideCollisionBlocker.Enabled = false;
return;
}
if (currentHull?.Submarine == null) { return; }
var connectedGaps = currentHull.ConnectedGaps.Where(g => !g.IsRoomToRoom);
foreach (Gap gap in connectedGaps)
@@ -943,23 +968,15 @@ namespace Barotrauma
else
{
if (character.Position.X < gap.Rect.X || character.Position.X > gap.Rect.Right) continue;
if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Center.X - currentHull.Rect.Height / 2)) !=
Math.Sign(character.Position.X - (currentHull.Rect.Center.X - currentHull.Rect.Height / 2)))
if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2)) !=
Math.Sign(character.Position.Y - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2)))
{
continue;
}
}
if (!gap.GetOutsideCollider(out Vector2? outsideColliderPos, out Vector2? outsideColliderNormal)) continue;
outsideCollisionBlocker.SetTransform(
outsideColliderPos.Value - currentHull.Submarine.SimPosition,
MathUtils.VectorToAngle(outsideColliderNormal.Value) - MathHelper.PiOver2);
outsideCollisionBlocker.Enabled = true;
return;
gap.RefreshOutsideCollider();
}
outsideCollisionBlocker.Enabled = false;
}
public void Teleport(Vector2 moveAmount, Vector2 velocityChange)
@@ -1001,7 +1018,7 @@ namespace Barotrauma
Physics.CollisionLevel | Physics.CollisionWall
: Physics.CollisionWall;
Category collisionCategory = (ignorePlatforms) ?
Category collisionCategory = (IgnorePlatforms) ?
wall | Physics.CollisionProjectile | Physics.CollisionStairs
: wall | Physics.CollisionProjectile | Physics.CollisionPlatform | Physics.CollisionStairs;
@@ -1053,6 +1070,12 @@ namespace Barotrauma
{
if (!character.Enabled || Frozen || Invalid) { return; }
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
}
CheckValidity();
UpdateNetPlayerPosition(deltaTime);
@@ -1072,6 +1095,7 @@ namespace Barotrauma
{
inWater = false;
headInWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
}
//ragdoll isn't in any room -> it's in the water
else if (currentHull == null)
@@ -1092,7 +1116,7 @@ namespace Barotrauma
}
else
{
floorY = GetFloorY();
RefreshFloorY(ignoreStairs: Stairs == null);
float waterSurface = ConvertUnits.ToSimUnits(currentHull.Surface);
if (targetMovement.Y < 0.0f)
{
@@ -1184,140 +1208,24 @@ namespace Barotrauma
limb.Update(deltaTime);
}
bool onStairs = Stairs != null;
Stairs = null;
var contacts = Collider.FarseerBody.ContactList;
while (Collider.FarseerBody.Enabled && contacts != null && contacts.Contact != null)
if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround)
{
if (contacts.Contact.Enabled && contacts.Contact.IsTouching)
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.height * 0.5f) + Collider.radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f && onGround)
{
contacts.Contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
switch (contacts.Contact.FixtureA.CollisionCategories)
if (Stairs != null)
{
case Physics.CollisionStairs:
Structure structure = contacts.Contact.FixtureA.Body.UserData as Structure;
if (structure != null && onStairs)
{
Stairs = structure;
}
break;
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
}
// case Physics.CollisionPlatform:
// Structure platform = contacts.Contact.FixtureA.Body.UserData as Structure;
// if (IgnorePlatforms || colliderBottom.Y < ConvertUnits.ToSimUnits(platform.Rect.Y - 15))
// {
// contacts = contacts.Next;
// continue;
// }
// break;
// case Physics.CollisionWall:
// break;
// default:
// contacts = contacts.Next;
// continue;
//}
if (points[0].Y < Collider.SimPosition.Y)
else
{
floorY = Math.Max(floorY, points[0].Y);
onGround = true;
onFloorTimer = 0.1f;
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
}
}
contacts = contacts.Next;
}
}
//the ragdoll "stays on ground" for 50 millisecs after separation
if (onFloorTimer <= 0.0f)
{
onGround = false;
}
else
{
onFloorTimer -= deltaTime;
}
Vector2 rayStart = Collider.SimPosition;
Vector2 rayEnd = rayStart;
rayEnd.Y -= Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor*1.2f;
Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
if (!inWater && !character.IsDead && character.Stun <= 0f && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance)
{
float closestFraction = 1.0f;
Fixture closestFixture = null;
GameMain.World.RayCast((fixture, point, normal, fraction) =>
{
switch (fixture.CollisionCategories)
{
case Physics.CollisionStairs:
Structure structure = fixture.Body.UserData as Structure;
if (inWater && targetMovement.Y < 0.5f) return -1;
if (colliderBottomDisplay.Y < structure.Rect.Y - structure.Rect.Height + 30 && TargetMovement.Y < 0.5f && !onStairs) return -1;
if (character.SelectedBy != null) return -1;
break;
case Physics.CollisionPlatform:
Structure platform = fixture.Body.UserData as Structure;
//ignore platforms if collider is below it
// OR allow the character to "lift" itself above it if heading upwards and not on stairs
if (IgnorePlatforms || (colliderBottomDisplay.Y < platform.Rect.Y - 16 && (targetMovement.Y <= 0.0f || onStairs))) return -1;
break;
case Physics.CollisionWall:
case Physics.CollisionLevel:
if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) return -1;
break;
default:
return -1;
}
if (fraction < closestFraction)
{
closestFraction = fraction;
closestFixture = fixture;
}
return closestFraction;
}
, rayStart, rayEnd);
if (closestFraction < 1.0f && closestFixture != null)
{
onGround = true;
switch (closestFixture.CollisionCategories)
{
case Physics.CollisionStairs:
Stairs = closestFixture.Body.UserData as Structure;
onStairs = true;
break;
}
float tfloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * closestFraction;
float targetY = tfloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.height * 0.5f) + Collider.radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f)
{
if (onStairs)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
}
else
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
}
}
}
}
UpdateProjSpecific(deltaTime);
UpdateProjSpecific(deltaTime, cam);
}
private void CheckBodyInRest(float deltaTime)
@@ -1381,24 +1289,21 @@ namespace Barotrauma
private bool CheckValidity(PhysicsBody body)
{
string errorMsg = null;
string bodyName = body.UserData is Limb limb ?
"Limb (" + limb.type + ")" :
"Collider";
if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f)
{
errorMsg = bodyName + " position invalid (" + body.SimPosition + ", character: " + character.Name + "), resetting the ragdoll.";
errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: " + character.Name + "), resetting the ragdoll.";
}
else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f)
{
errorMsg = bodyName + " velocity invalid (" + body.LinearVelocity + ", character: " + character.Name + "), resetting the ragdoll.";
errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: " + character.Name + "), resetting the ragdoll.";
}
else if (!MathUtils.IsValid(body.Rotation))
{
errorMsg = bodyName + " rotation invalid (" + body.Rotation + ", character: " + character.Name + "), resetting the ragdoll.";
errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: " + character.Name + "), resetting the ragdoll.";
}
else if (!MathUtils.IsValid(body.AngularVelocity) || Math.Abs(body.AngularVelocity) > 1000f)
{
errorMsg = bodyName + " angular velocity invalid (" + body.AngularVelocity + ", character: " + character.Name + "), resetting the ragdoll.";
errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: " + character.Name + "), resetting the ragdoll.";
}
if (errorMsg != null)
{
@@ -1434,65 +1339,121 @@ namespace Barotrauma
SetInitialLimbPositions();
return false;
}
string GetBodyName()
{
return body.UserData is Limb limb ? "Limb (" + limb.type + ")" : "Collider";
}
return true;
}
partial void UpdateProjSpecific(float deltaTime);
partial void UpdateProjSpecific(float deltaTime, Camera cam);
partial void Splash(Limb limb, Hull limbHull);
protected float GetFloorY(Limb refLimb = null, bool ignoreStairs = false)
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
{
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
return GetFloorY(refBody.SimPosition, ignoreStairs);
if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms)
{
floorY = GetFloorY(refBody.SimPosition, ignoreStairs);
lastFloorCheckPos = refBody.SimPosition;
lastFloorCheckIgnoreStairs = ignoreStairs;
lastFloorCheckIgnorePlatforms = IgnorePlatforms;
}
}
protected float GetFloorY(Vector2 simPosition, bool ignoreStairs = false)
private float GetFloorY(Vector2 simPosition, bool ignoreStairs = false)
{
onGround = false;
Stairs = null;
Vector2 rayStart = simPosition;
float height = ColliderHeightFromFloor;
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) height = Math.Max(height, HeadPosition.Value);
if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) height = Math.Max(height, TorsoPosition.Value);
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); }
Vector2 rayEnd = rayStart - new Vector2(0.0f, height);
Vector2 onGroundRayEnd = rayStart - Vector2.UnitY * (Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f);
Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
Fixture standOnFloorFixture = null;
float standOnFloorFraction = 1;
float closestFraction = 1;
GameMain.World.RayCast((fixture, point, normal, fraction) =>
{
switch (fixture.CollisionCategories)
{
case Physics.CollisionStairs:
if (ignoreStairs) return -1;
if (inWater && TargetMovement.Y < 0.5f) return -1;
if (inWater && TargetMovement.Y < 0.5f) { return -1; }
if (character.SelectedBy == null && fraction < standOnFloorFraction)
{
Structure structure = fixture.Body.UserData as Structure;
if (colliderBottomDisplay.Y >= structure.Rect.Y - structure.Rect.Height + 30 || TargetMovement.Y > 0.5f || Stairs != null)
{
standOnFloorFraction = fraction;
standOnFloorFixture = fixture;
}
}
if (ignoreStairs) { return -1; }
break;
case Physics.CollisionPlatform:
Structure platform = fixture.Body.UserData as Structure;
if (!IgnorePlatforms && fraction < standOnFloorFraction)
{
if (colliderBottomDisplay.Y >= platform.Rect.Y - 16 || (targetMovement.Y > 0.0f && Stairs == null))
{
standOnFloorFraction = fraction;
standOnFloorFixture = fixture;
}
}
if (colliderBottomDisplay.Y < platform.Rect.Y - 16 && (targetMovement.Y <= 0.0f || Stairs != null)) return -1;
if (IgnorePlatforms && TargetMovement.Y < -0.5f || Collider.Position.Y < platform.Rect.Y) return -1;
break;
case Physics.CollisionWall:
case Physics.CollisionLevel:
if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) return -1;
if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) { return -1; }
if (fraction < standOnFloorFraction)
{
standOnFloorFraction = fraction;
standOnFloorFixture = fixture;
}
break;
default:
System.Diagnostics.Debug.Assert(false, "Floor raycast should not have hit a fixture with the collision category " + fixture.CollisionCategories);
return -1;
}
if (fraction < closestFraction)
{
floorNormal = normal;
closestFraction = fraction;
}
return closestFraction;
}
, rayStart, rayEnd);
}, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel);
if (standOnFloorFixture != null)
{
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f)
{
onGround = true;
if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs)
{
Stairs = standOnFloorFixture.Body.UserData as Structure;
}
}
}
if (closestFraction == 1) //raycast didn't hit anything
{
floorNormal = Vector2.UnitY;
return (currentHull == null) ? -1000.0f : ConvertUnits.ToSimUnits(currentHull.Rect.Y - currentHull.Rect.Height);
}
else
@@ -1514,7 +1475,7 @@ namespace Barotrauma
}
if (MainLimb == null) { return; }
Vector2 limbMoveAmount = simPosition - MainLimb.SimPosition;
Vector2 limbMoveAmount = simPosition - Collider.SimPosition;
if (lerp)
{
@@ -1736,17 +1697,14 @@ namespace Barotrauma
collider = null;
}
if (outsideCollisionBlocker != null)
{
GameMain.World.RemoveBody(outsideCollisionBlocker);
outsideCollisionBlocker = null;
}
if (LimbJoints != null)
{
foreach (RevoluteJoint joint in LimbJoints)
{
GameMain.World.RemoveJoint(joint);
if (GameMain.World.JointList.Contains(joint))
{
GameMain.World.Remove(joint);
}
}
LimbJoints = null;
}
@@ -78,7 +78,7 @@ namespace Barotrauma
[Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable]
public AttackTarget TargetType { get; private set; }
[Serialize(LimbType.None, true, description: "If not defined or set to none, the closest limb is used (default)."), Editable]
[Serialize(LimbType.None, true, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable]
public LimbType TargetLimbType { get; private set; }
[Serialize(HitDetection.Distance, true, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable]
@@ -87,9 +87,12 @@ namespace Barotrauma
[Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable]
public AIBehaviorAfterAttack AfterAttack { get; set; }
[Serialize(false, true, description: "Should the AI try to reverse when aiming with this attack?"), Editable]
[Serialize(false, true, description: "Should the AI try to turn around when aiming with this attack?"), Editable]
public bool Reverse { get; private set; }
[Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable]
public bool Retreat { get; private set; }
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
public float Range { get; set; }
@@ -147,7 +150,7 @@ namespace Barotrauma
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Force { get; private set; }
[Serialize(0.0f, true, description: "Applied to the attacking limb."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Torque { get; private set; }
[Serialize(false, true), Editable]
@@ -279,7 +282,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Name.ToLowerInvariant() == afflictionName);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
@@ -289,7 +292,7 @@ namespace Barotrauma
else
{
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
@@ -305,7 +308,10 @@ namespace Barotrauma
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
{
Conditionals.Add(new PropertyConditional(attribute));
if (PropertyConditional.IsValid(attribute))
{
Conditionals.Add(new PropertyConditional(attribute));
}
}
break;
}
@@ -321,7 +327,7 @@ namespace Barotrauma
AfflictionPrefab afflictionPrefab;
Affliction affliction;
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab != null)
{
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
@@ -57,6 +57,9 @@ namespace Barotrauma
public Hull CurrentHull = null;
public bool IsRemotePlayer;
public bool IsPlayer => Controlled == this || IsRemotePlayer;
public readonly Dictionary<string, SerializableProperty> Properties;
public Dictionary<string, SerializableProperty> SerializableProperties
{
@@ -69,7 +72,7 @@ namespace Barotrauma
}
protected Key[] keys;
private Item[] selectedItems;
private readonly Item[] selectedItems;
public enum TeamType
{
@@ -99,10 +102,12 @@ namespace Barotrauma
//seed used to generate this character
private readonly string seed;
protected Item focusedItem;
private Character focusedCharacter, selectedCharacter, selectedBy;
private Character selectedCharacter, selectedBy;
public Character LastAttacker;
public Entity LastDamageSource;
private CharacterPrefab prefab;
public readonly CharacterParams Params;
public string SpeciesName => Params.SpeciesName;
public bool IsHumanoid => Params.Humanoid;
@@ -126,22 +131,45 @@ namespace Barotrauma
set => Params.Noise = value;
}
public float Visibility
{
get => Params.Visibility;
set => Params.Visibility = value;
}
public bool IsTraitor;
public string TraitorCurrentObjective = "";
public bool IsHuman => SpeciesName.Equals(HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
private float attackCoolDown;
private Order currentOrder;
public Order CurrentOrder
{
get { return currentOrder; }
get
{
return Info?.CurrentOrder;
}
private set
{
if (Info != null) { Info.CurrentOrder = value; }
}
}
public string CurrentOrderOption
{
get
{
return Info?.CurrentOrderOption;
}
private set
{
if (Info != null) { Info.CurrentOrderOption = value; }
}
}
private string currentOrderOption;
private List<StatusEffect> statusEffects = new List<StatusEffect>();
private List<float> speedMultipliers = new List<float>();
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
private readonly List<float> speedMultipliers = new List<float>();
private float greatestNegativeSpeedMultiplier = 1f;
private float greatestPositiveSpeedMultiplier = 1f;
public Entity ViewTarget
{
@@ -302,11 +330,7 @@ namespace Barotrauma
get { return Submarine == null ? cursorPosition : cursorPosition + Submarine.Position; }
}
public Character FocusedCharacter
{
get { return focusedCharacter; }
set { focusedCharacter = value; }
}
public Character FocusedCharacter { get; set; }
public Character SelectedCharacter
{
@@ -319,6 +343,10 @@ namespace Barotrauma
selectedCharacter = value;
if (selectedCharacter != null)
selectedCharacter.selectedBy = this;
#if CLIENT
CharacterHealth.SetHealthBarVisibility(value == null);
#endif
}
}
@@ -620,7 +648,7 @@ namespace Barotrauma
speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant();
}
Character newCharacter = null;
if (!speciesName.Equals(HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
{
var aiCharacter = new AICharacter(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll);
var ai = new EnemyAIController(aiCharacter, seed);
@@ -679,11 +707,8 @@ namespace Barotrauma
protected Character(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
: base(null)
{
string path = GetConfigFilePath(speciesName);
if (!TryGetConfigFile(path, out XDocument doc))
{
throw new Exception($"Failed to load the config file for {speciesName} from {path}!");
}
prefab = CharacterPrefab.FindBySpeciesName(speciesName);
this.seed = seed;
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
@@ -697,14 +722,14 @@ namespace Barotrauma
lowPassMultiplier = 1.0f;
Properties = SerializableProperty.GetProperties(this);
Params = new CharacterParams(path);
Params = new CharacterParams(prefab.FilePath);
Info = characterInfo;
if (speciesName.Equals(HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
if (speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
{
if (characterInfo == null)
{
Info = new CharacterInfo(HumanSpeciesName);
Info = new CharacterInfo(CharacterPrefab.HumanSpeciesName);
}
}
@@ -714,7 +739,7 @@ namespace Barotrauma
keys[i] = new Key((InputType)i);
}
var rootElement = doc.Root;
var rootElement = prefab.XDocument.Root;
var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
InitProjSpecific(mainElement);
@@ -763,13 +788,13 @@ namespace Barotrauma
var matchingAffliction = AfflictionPrefab.List
.Where(p => p.AfflictionType == "huskinfection")
.Select(p => p as AfflictionPrefabHusk)
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.InvariantCultureIgnoreCase)));
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase)));
string nonHuskedSpeciesName = string.Empty;
if (matchingAffliction == null)
{
DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!");
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
nonHuskedSpeciesName = IsHumanoid ? HumanSpeciesName : "crawler";
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler";
}
else
{
@@ -784,9 +809,10 @@ namespace Barotrauma
if (IsHumanoid)
{
AnimController = new HumanoidAnimController(this, seed, ragdollParams as HumanRagdollParams);
AnimController.TargetDir = Direction.Right;
AnimController = new HumanoidAnimController(this, seed, ragdollParams as HumanRagdollParams)
{
TargetDir = Direction.Right
};
}
else
{
@@ -797,7 +823,7 @@ namespace Barotrauma
AnimController.SetPosition(ConvertUnits.ToSimUnits(position));
AnimController.FindHull(null);
if (AnimController.CurrentHull != null) Submarine = AnimController.CurrentHull.Submarine;
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
CharacterList.Add(this);
@@ -850,140 +876,10 @@ namespace Barotrauma
#if CLIENT
head.LoadHuskSprite();
head.LoadHerpesSprite();
head.UpdateWearableTypesToHide();
#endif
}
public static string HumanSpeciesName = "human";
public static string HumanConfigFile => GetConfigFilePath(HumanSpeciesName);
/// <summary>
/// Searches for a character config file from all currently selected content packages,
/// or from a specific package if the contentPackage parameter is given.
/// </summary>
public static string GetConfigFilePath(string speciesName, ContentPackage contentPackage = null)
{
if (configFilePaths.None() || configFiles.None())
{
LoadAllConfigFiles();
}
string configFile = null;
if (contentPackage != null)
{
configFile = contentPackage.GetFilesOfType(ContentType.Character)?
.FirstOrDefault(c => Path.GetFileName(c).ToLowerInvariant() == $"{speciesName.ToLowerInvariant()}.xml");
if (configFile == null)
{
DebugConsole.ThrowError($"Couldn't find a config file for {speciesName} from the specified content package {contentPackage.Name} defined in {contentPackage.Path}!");
DebugConsole.ThrowError($"(The config file must end with \"{speciesName}.xml\")");
return string.Empty;
}
}
else
{
if (!configFilePaths.TryGetValue(speciesName.ToLowerInvariant(), out configFile))
{
DebugConsole.ThrowError($"Couldn't find a config file for species \"{speciesName}\" from the selected content packages!");
}
}
return configFile;
}
private readonly static Dictionary<string, string> configFilePaths = new Dictionary<string, string>();
private readonly static Dictionary<string, XDocument> configFiles = new Dictionary<string, XDocument>();
public static IEnumerable<string> ConfigFilePaths => configFiles.Keys;
public static IEnumerable<XDocument> ConfigFiles => configFiles.Values;
public static bool TryAddConfigFile(string file, bool forceOverride)
{
if (configFilePaths.None() || configFiles.None())
{
LoadAllConfigFiles();
}
return AddConfigFile(file, forceOverride);
}
private static bool AddConfigFile(string file, bool forceOverride = false)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null)
{
DebugConsole.ThrowError($"Loading character file failed: {file}");
return false;
}
if (configFilePaths.ContainsKey(file))
{
DebugConsole.ThrowError($"Duplicate path: {file}");
return false;
}
XElement mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
var name = mainElement.GetAttributeString("name", null);
if (name != null)
{
DebugConsole.NewMessage($"Error in {file}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
name = mainElement.GetAttributeString("speciesname", string.Empty);
}
if (string.IsNullOrWhiteSpace(name))
{
DebugConsole.ThrowError($"No species name defined for: {file}");
return false;
}
name = name.ToLowerInvariant();
var duplicate = configFiles.FirstOrDefault(kvp => (kvp.Value.Root.IsOverride() ? kvp.Value.Root.FirstElement() : kvp.Value.Root)
.GetAttributeString("speciesname", string.Empty).Equals(name, StringComparison.OrdinalIgnoreCase));
if (duplicate.Value != null)
{
if (forceOverride || doc.Root.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing character '{name}' defined in '{duplicate.Key}' with '{file}'", Color.Yellow);
configFiles.Remove(duplicate.Key);
configFilePaths.Remove(name);
}
else
{
DebugConsole.ThrowError($"Duplicate species name '{name}' in '{file}'! Add <override></override> tags as the parent of the character definition to override an existing character.");
return false;
}
}
configFiles.Add(file, doc);
configFilePaths.Add(name, file);
return true;
}
public static XDocument GetConfigFile(string speciesName)
{
string file = GetConfigFilePath(speciesName);
if (!TryGetConfigFile(file, out XDocument doc))
{
DebugConsole.ThrowError($"Failed to load the config file for {speciesName} from {file}!");
}
return doc;
}
public static bool TryGetConfigFile(string file, out XDocument doc)
{
doc = null;
if (configFiles.None()) { LoadAllConfigFiles(); }
if (string.IsNullOrWhiteSpace(file)) { return false; }
configFiles.TryGetValue(file, out doc);
return doc != null;
}
public static void LoadAllConfigFiles()
{
configFiles.Clear();
configFilePaths.Clear();
foreach (var file in ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Character))
{
AddConfigFile(file);
}
}
public bool IsKeyHit(InputType inputType)
{
#if SERVER
@@ -1120,8 +1016,7 @@ namespace Barotrauma
public void GiveJobItems(WayPoint spawnPoint = null)
{
if (info == null || info.Job == null) return;
if (info == null || info.Job == null) { return; }
info.Job.GiveJobItems(this, spawnPoint);
}
@@ -1156,7 +1051,7 @@ namespace Barotrauma
if (AnimController.InWater)
{
float length = targetMovement.Length();
if (length > 0.0f) targetMovement = targetMovement / length;
if (length > 0.0f) targetMovement /= length;
}
bool run = false;
@@ -1195,42 +1090,32 @@ namespace Barotrauma
{
get
{
if (speedMultipliers.Count == 0) return 1f;
float greatestPositive = 1f;
float greatestNegative = 1f;
for (int i = 0; i < speedMultipliers.Count; i++)
{
float val = speedMultipliers[i];
if (val < 1f)
{
if (val < greatestNegative)
{
greatestNegative = val;
}
}
else
{
if (val > greatestPositive)
{
greatestPositive = val;
}
}
}
return greatestPositive - (1f - greatestNegative);
return greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier);
}
set
}
public void StackSpeedMultiplier(float val)
{
if (val < 1f)
{
if (value == 1f) return;
speedMultipliers.Add(value);
if (val < greatestNegativeSpeedMultiplier)
{
greatestNegativeSpeedMultiplier = val;
}
}
else
{
if (val > greatestPositiveSpeedMultiplier)
{
greatestPositiveSpeedMultiplier = val;
}
}
}
public void ResetSpeedMultiplier()
{
speedMultipliers.Clear();
greatestPositiveSpeedMultiplier = 1f;
greatestNegativeSpeedMultiplier = 1f;
}
public float ApplyTemporarySpeedLimits(float speed)
@@ -1332,7 +1217,7 @@ namespace Barotrauma
}
}
#if DEBUG
#if DEBUG && CLIENT
if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.F))
{
AnimController.ReleaseStuckLimbs();
@@ -1443,14 +1328,14 @@ namespace Barotrauma
{
SelectedConstruction.SecondaryUse(deltaTime, this);
}
if (IsKeyDown(InputType.Use) && !SelectedConstruction.IsShootable)
if (IsKeyDown(InputType.Use) && SelectedConstruction != null && !SelectedConstruction.IsShootable)
{
if (!SelectedConstruction.RequireAimToUse || IsKeyDown(InputType.Aim))
{
SelectedConstruction.Use(deltaTime, this);
}
}
if (IsKeyDown(InputType.Shoot) && SelectedConstruction.IsShootable)
if (IsKeyDown(InputType.Shoot) && SelectedConstruction != null && SelectedConstruction.IsShootable)
{
if (!SelectedConstruction.RequireAimToUse || IsKeyDown(InputType.Aim))
{
@@ -1478,6 +1363,7 @@ namespace Barotrauma
public bool CanSeeCharacter(Character target)
{
if (target.Removed) { return false; }
Limb seeingLimb = GetSeeingLimb();
return target.AnimController.Limbs.Any(l => CanSeeTarget(l, seeingLimb));
}
@@ -1565,6 +1451,7 @@ namespace Barotrauma
public bool HasEquippedItem(Item item)
{
if (Inventory == null) { return false; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) return true;
@@ -1719,7 +1606,7 @@ namespace Barotrauma
public bool CanInteractWith(Item item)
{
return CanInteractWith(item, out float distanceToItem, checkLinked: true);
return CanInteractWith(item, out _, checkLinked: true);
}
public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinked)
@@ -1773,7 +1660,7 @@ namespace Barotrauma
if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; }
Pickable pickableComponent = item.GetComponent<Pickable>();
if (pickableComponent != null && (pickableComponent.Picker != null && !pickableComponent.Picker.IsDead)) { return false; }
if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; }
Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation));
@@ -1903,7 +1790,7 @@ namespace Barotrauma
focusedItem = null;
if (!AllowInput)
{
focusedCharacter = null;
FocusedCharacter = null;
if (SelectedCharacter != null) DeselectCharacter();
return;
}
@@ -1917,9 +1804,14 @@ namespace Barotrauma
{
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
{
focusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null;
focusedItem = CanInteract ?
FindItemAtPosition(mouseSimPos, GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f)) : null;
FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null;
float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (SelectedItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
{
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
aimAssist = 0.0f;
}
focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
findFocusedTimer = 0.05f;
}
}
@@ -1930,10 +1822,14 @@ namespace Barotrauma
findFocusedTimer -= deltaTime;
}
#endif
var head = AnimController.GetLimb(LimbType.Head);
bool headInWater = head == null ?
AnimController.InWater :
head.inWater;
//climb ladders automatically when pressing up/down inside their trigger area
Ladder currentLadder = SelectedConstruction?.GetComponent<Ladder>();
if ((SelectedConstruction == null || currentLadder != null) &&
!AnimController.InWater && Screen.Selected != GameMain.SubEditorScreen)
!headInWater && Screen.Selected != GameMain.SubEditorScreen)
{
bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down);
bool isControlled = Controlled == this;
@@ -1977,13 +1873,13 @@ namespace Barotrauma
{
DeselectCharacter();
}
else if (focusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && CanInteract)
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && CanInteract)
{
SelectCharacter(focusedCharacter);
SelectCharacter(FocusedCharacter);
}
else if (focusedCharacter != null && IsKeyHit(InputType.Health) && focusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(focusedCharacter, 160f, false))
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false))
{
if (focusedCharacter == SelectedCharacter)
if (FocusedCharacter == SelectedCharacter)
{
DeselectCharacter();
#if CLIENT
@@ -1992,15 +1888,15 @@ namespace Barotrauma
}
else
{
SelectCharacter(focusedCharacter);
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this) CharacterHealth.OpenHealthWindow = focusedCharacter.CharacterHealth;
if (Controlled == this) CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
#endif
}
}
else if (focusedCharacter != null && IsKeyHit(InputType.Select) && FocusedCharacter.onCustomInteract != null)
else if (FocusedCharacter != null && IsKeyHit(InputType.Select) && FocusedCharacter.onCustomInteract != null)
{
FocusedCharacter.onCustomInteract(focusedCharacter, this);
FocusedCharacter.onCustomInteract(FocusedCharacter, this);
}
else if (focusedItem != null)
{
@@ -2055,33 +1951,16 @@ namespace Barotrauma
}
else
{
float distSqr = float.MaxValue;
foreach (Character otherCharacter in CharacterList)
{
if (otherCharacter == c || !otherCharacter.IsRemotePlayer) { continue; }
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.WorldPosition, c.WorldPosition));
}
#if SERVER
for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++)
{
var spectatePos = GameMain.Server.ConnectedClients[i].SpectatePos;
if (spectatePos != null)
{
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(spectatePos.Value, c.WorldPosition));
}
}
#endif
if (distSqr > NetConfig.DisableCharacterDistSqr)
float closestPlayerDist = c.GetDistanceToClosestPlayer();
if (closestPlayerDist > NetConfig.DisableCharacterDist)
{
c.Enabled = false;
if (c.IsDead && c.AIController is EnemyAIController)
{
Entity.Spawner?.AddToRemoveQueue(c);
Spawner?.AddToRemoveQueue(c);
}
}
else if (distSqr < NetConfig.EnableCharacterDistSqr)
else if (closestPlayerDist < NetConfig.EnableCharacterDist)
{
c.Enabled = true;
}
@@ -2128,6 +2007,8 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
UpdateDespawn(deltaTime);
if (!Enabled) { return; }
if (Level.Loaded != null && WorldPosition.Y < Level.MaxEntityDepth ||
@@ -2161,8 +2042,8 @@ namespace Barotrauma
HideFace = false;
UpdateSightRange();
UpdateSoundRange();
UpdateSightRange(deltaTime);
UpdateSoundRange(deltaTime);
if (IsDead) { return; }
@@ -2233,12 +2114,12 @@ namespace Barotrauma
UpdateControlled(deltaTime, cam);
//Health effects
if (NeedsAir) UpdateOxygen(deltaTime);
if (NeedsAir) { UpdateOxygen(deltaTime); }
CharacterHealth.Update(deltaTime);
if (IsUnconscious)
{
UpdateUnconscious(deltaTime);
UpdateUnconscious();
return;
}
@@ -2311,6 +2192,8 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam);
partial void SetOrderProjSpecific(Order order, string orderOption);
private void UpdateOxygen(float deltaTime)
{
PressureProtection -= deltaTime * 100.0f;
@@ -2330,7 +2213,7 @@ namespace Barotrauma
}
partial void UpdateOxygenProjSpecific(float prevOxygen);
private void UpdateUnconscious(float deltaTime)
private void UpdateUnconscious()
{
Stun = Math.Max(5.0f, Stun);
@@ -2338,47 +2221,183 @@ namespace Barotrauma
SelectedConstruction = null;
}
private void UpdateSightRange()
/// <summary>
/// How far the character is from the closest human player (including spectators)
/// </summary>
private float GetDistanceToClosestPlayer()
{
if (aiTarget == null) { return; }
float range = (float)Math.Sqrt(Mass) * 250 + AnimController.Collider.LinearVelocity.Length() * 500;
aiTarget.SightRange = MathHelper.Clamp(range, 0, 10000);
float distSqr = float.MaxValue;
foreach (Character otherCharacter in CharacterList)
{
if (otherCharacter == this || !otherCharacter.IsRemotePlayer) { continue; }
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.WorldPosition, WorldPosition));
}
#if SERVER
for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++)
{
var spectatePos = GameMain.Server.ConnectedClients[i].SpectatePos;
if (spectatePos != null)
{
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(spectatePos.Value, WorldPosition));
}
}
#else
if (this == Controlled) { return 0.0f; }
if (controlled != null)
{
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, WorldPosition));
}
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.Position, WorldPosition));
#endif
return (float)Math.Sqrt(distSqr);
}
private void UpdateSoundRange()
private float despawnTimer;
private const float DespawnDelay = 5.0f * 60.0f; //5 minutes
private void UpdateDespawn(float deltaTime)
{
//clients don't despawn characters unless the server says so
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (!IsDead) { return; }
float distToClosestPlayer = GetDistanceToClosestPlayer();
if (distToClosestPlayer > NetConfig.DisableCharacterDist)
{
//despawn in 1 second if very far from all human players
despawnTimer = Math.Max(despawnTimer, DespawnDelay - 1.0f);
}
despawnTimer += deltaTime;
if (despawnTimer < DespawnDelay) { return; }
if (IsHuman)
{
var containerPrefab =
ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ??
(MapEntityPrefab.Find(null, identifier: "metalcrate") as ItemPrefab);
if (containerPrefab == null)
{
DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red);
}
else
{
Spawner.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned);
}
void onItemContainerSpawned(Item item)
{
if (Inventory?.Items == null) { return; }
item.AddTag("name:" + Name);
if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); }
var itemContainer = item?.GetComponent<ItemContainer>();
if (itemContainer == null) { return; }
foreach (Item inventoryItem in Inventory.Items)
{
if (inventoryItem == null) { continue; }
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null))
{
//if the item couldn't be put inside the despawn container, just drop it
inventoryItem.Drop(dropper: this);
}
}
}
}
Spawner.AddToRemoveQueue(this);
}
public void DespawnNow()
{
despawnTimer = DespawnDelay;
}
public static void RemoveByPrefab(CharacterPrefab prefab)
{
if (CharacterList == null) { return; }
List<Character> list = new List<Character>(CharacterList);
foreach (Character character in list)
{
if (character.prefab == prefab)
{
character.Remove();
}
}
}
private readonly float maxAIRange = 10000;
private readonly float aiTargetChangeSpeed = 5;
private void UpdateSightRange(float deltaTime)
{
if (aiTarget == null) { return; }
float range = ((float)Math.Sqrt(Mass) / 3) * (AnimController.TargetMovement.Length() * 2) * Noise;
aiTarget.SoundRange = MathHelper.Clamp(range, 0, 10000);
float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000);
float massFactor = (float)Math.Sqrt(Mass / 20);
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
if (!float.IsNaN(newRange))
{
aiTarget.SightRange = newRange;
}
}
private void UpdateSoundRange(float deltaTime)
{
if (aiTarget == null) { return; }
if (IsDead)
{
aiTarget.SoundRange = 0;
}
else
{
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
}
}
}
public bool CanHearCharacter(Character speaker)
{
if (speaker == null || speaker.SpeechImpediment > 100.0f) { return false; }
if (speaker == this) { return true; }
ChatMessageType messageType = ChatMessage.CanUseRadio(speaker) && ChatMessage.CanUseRadio(this) ?
ChatMessageType.Radio :
ChatMessageType.Default;
return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this));
}
public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true)
{
if (orderGiver != null)
//set the character order only if the character is close enough to hear the message
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (AIController is HumanAIController humanAI)
{
//set the character order only if the character is close enough to hear the message
ChatMessageType messageType = ChatMessage.CanUseRadio(orderGiver) && ChatMessage.CanUseRadio(this) ?
ChatMessageType.Radio : ChatMessageType.Default;
if (string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, orderGiver, this))) return;
humanAI.SetOrder(order, orderOption, orderGiver, speak);
}
HumanAIController humanAI = AIController as HumanAIController;
humanAI?.SetOrder(order, orderOption, orderGiver, speak);
currentOrder = order;
currentOrderOption = orderOption;
SetOrderProjSpecific(order, orderOption);
CurrentOrder = order;
CurrentOrderOption = orderOption;
}
private List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
private List<AIChatMessage> prevAiChatMessages = new List<AIChatMessage>();
private readonly List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
//key = identifier, value = time the message was sent
private readonly Dictionary<string, float> prevAiChatMessages = new Dictionary<string, float>();
public void DisableLine(string identifier)
{
var dummyMsg = new AIChatMessage("", ChatMessageType.Default, identifier)
if (!string.IsNullOrEmpty(identifier))
{
SendTime = Timing.TotalTime
};
prevAiChatMessages.Add(dummyMsg);
prevAiChatMessages[identifier] = (float)Timing.TotalTime;
}
}
public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f)
@@ -2386,10 +2405,15 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (string.IsNullOrEmpty(message)) { return; }
if (prevAiChatMessages.ContainsKey(identifier) &&
prevAiChatMessages[identifier] < Timing.TotalTime - minDurationBetweenSimilar)
{
prevAiChatMessages.Remove(identifier);
}
//already sent a similar message a moment ago
if (!string.IsNullOrEmpty(identifier) && minDurationBetweenSimilar > 0.0f &&
(aiChatMessageQueue.Any(m => m.Identifier == identifier) ||
prevAiChatMessages.Any(m => m.Identifier == identifier && m.SendTime > Timing.TotalTime - minDurationBetweenSimilar)))
(aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier)))
{
return;
}
@@ -2434,15 +2458,25 @@ namespace Barotrauma
{
sent.SendTime = Timing.TotalTime;
aiChatMessageQueue.Remove(sent);
prevAiChatMessages.Add(sent);
if (!string.IsNullOrEmpty(sent.Identifier))
{
prevAiChatMessages[sent.Identifier] = (float)sent.SendTime;
}
}
for (int i = prevAiChatMessages.Count - 1; i >= 0; i--)
if (prevAiChatMessages.Count > 100)
{
if (prevAiChatMessages[i].SendTime < Timing.TotalTime - 60.0f)
List<string> toRemove = new List<string>();
foreach (KeyValuePair<string,float> prevMessage in prevAiChatMessages)
{
prevAiChatMessages.RemoveRange(0, i + 1);
break;
if (prevMessage.Value < Timing.TotalTime - 60.0f)
{
toRemove.Add(prevMessage.Key);
}
}
foreach (string identifier in toRemove)
{
prevAiChatMessages.Remove(identifier);
}
}
}
@@ -2454,8 +2488,6 @@ namespace Barotrauma
speechBubbleColor = color;
}
partial void DamageHUD(float amount);
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
@@ -2621,6 +2653,7 @@ namespace Barotrauma
mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
CharacterHealth.ApplyDamage(hitLimb, attackResult);
@@ -2628,6 +2661,10 @@ namespace Barotrauma
{
OnAttacked?.Invoke(attacker, attackResult);
OnAttackedProjSpecific(attacker, attackResult);
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, -attackResult.Damage);
}
};
if (attacker != null && attackResult.Damage > 0.0f)
@@ -2640,6 +2677,30 @@ namespace Barotrauma
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult);
public void TryAdjustAttackerSkill(Character attacker, float healthChange)
{
if (attacker == null) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
if (isEnemy)
{
if (healthChange < 0.0f)
{
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
attacker.Info?.IncreaseSkillLevel("weapons",
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f),
attacker.WorldPosition + Vector2.UnitY * 100.0f);
}
}
else if (healthChange > 0.0f)
{
float attackerSkillLevel = attacker.GetSkillLevel("medical");
attacker.Info?.IncreaseSkillLevel("medical",
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f),
attacker.WorldPosition + Vector2.UnitY * 100.0f);
}
}
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
@@ -2875,7 +2936,7 @@ namespace Barotrauma
foreach (Character c in CharacterList)
{
if (c.focusedCharacter == this) { c.focusedCharacter = null; }
if (c.FocusedCharacter == this) { c.FocusedCharacter = null; }
if (c.SelectedCharacter == this) { c.SelectedCharacter = null; }
}
}
@@ -2914,7 +2975,7 @@ namespace Barotrauma
}
}
private HashSet<AttackContext> currentContexts = new HashSet<AttackContext>();
private readonly HashSet<AttackContext> currentContexts = new HashSet<AttackContext>();
public IEnumerable<AttackContext> GetAttackContexts()
{
@@ -260,31 +260,6 @@ namespace Barotrauma
}
}
private Sprite portraitBackground;
public Sprite PortraitBackground
{
get
{
if (portraitBackground == null)
{
var portraitBackgroundElement = CharacterConfigElement.Element("portraitbackground");
if (portraitBackgroundElement != null)
{
portraitBackground = new Sprite(portraitBackgroundElement.Element("sprite"));
}
}
return portraitBackground;
}
private set
{
if (portraitBackground != null)
{
portraitBackground.Remove();
}
portraitBackground = value;
}
}
private List<WearableSprite> attachmentSprites;
public List<WearableSprite> AttachmentSprites
{
@@ -318,6 +293,9 @@ namespace Barotrauma
private NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set;}
public string CurrentOrderOption { get; set; }
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
@@ -406,7 +384,7 @@ namespace Barotrauma
if (ragdoll == null)
{
string speciesName = SpeciesName;
bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(Character.HumanSpeciesName, StringComparison.OrdinalIgnoreCase));
bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase));
ragdoll = isHumanoid
? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName)
: RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName, ragdollFileName) as RagdollParams;
@@ -429,7 +407,7 @@ namespace Barotrauma
idCounter++;
_speciesName = speciesName;
SpriteTags = new List<string>();
XDocument doc = Character.GetConfigFile(_speciesName);
XDocument doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument;
if (doc == null) { return; }
CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
head = new HeadInfo();
@@ -442,6 +420,7 @@ namespace Barotrauma
CalculateHeadSpriteRange();
Head.HeadSpriteId = GetRandomHeadID();
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab, variant);
if (!string.IsNullOrEmpty(name))
{
Name = name;
@@ -490,7 +469,7 @@ namespace Barotrauma
XDocument doc = null;
if (_speciesName != null)
{
doc = Character.GetConfigFile(_speciesName);
doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument;
}
else
{
@@ -548,9 +527,11 @@ namespace Barotrauma
}
foreach (XElement subElement in infoElement.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
Job = new Job(subElement);
break;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase))
{
Job = new Job(subElement);
break;
}
}
LoadHeadAttachments();
}
@@ -685,7 +666,7 @@ namespace Barotrauma
{
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; }
if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
XElement spriteElement = limbElement.Element("sprite");
if (spriteElement == null) { continue; }
@@ -701,7 +682,7 @@ namespace Barotrauma
//go through the files in the directory to find a matching sprite
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
{
if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -852,6 +833,11 @@ namespace Barotrauma
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
if (Job.Prefab.Identifier == "assistant")
{
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
}
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase);
@@ -944,8 +930,20 @@ namespace Barotrauma
DebugConsole.ThrowError("Invalid inventory data in character \"" + Name + "\" - no slot indices found");
continue;
}
//make sure there's no other item in the slot
//this should not happen normally, but can occur if the character is accidentally given new job items while also loading previous items in the campaign
for (int i = 0; i < inventory.Capacity; i++)
{
if (slotIndices.Contains(i) && inventory.Items[i] != null && inventory.Items[i] != newItem)
{
DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{inventory.Items[i].Name} ({inventory.Items[i].ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\"");
inventory.Items[i].Drop(null, createNetworkEvent: false);
}
}
inventory.TryPutItem(newItem, slotIndices[0], false, false, null);
newItem.ParentInventory = inventory;
//force the item to the correct slots
// e.g. putting the item in a hand slot will also put it in the first available Any-slot,
@@ -967,7 +965,7 @@ namespace Barotrauma
foreach (XElement childInvElement in itemElement.Elements())
{
if (itemContainerIndex >= itemContainers.Count) break;
if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue;
if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement);
itemContainerIndex++;
}
@@ -1004,7 +1002,6 @@ namespace Barotrauma
Character = null;
HeadSprite = null;
Portrait = null;
PortraitBackground = null;
AttachmentSprites = null;
}
}
@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class CharacterPrefab : IPrefab, IDisposable
{
public readonly static PrefabCollection<CharacterPrefab> Prefabs = new PrefabCollection<CharacterPrefab>();
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
Character.RemoveByPrefab(this);
}
public string OriginalName { get; private set; }
public string Name { get; private set; }
public string Identifier { get; private set; }
public string FilePath { get; private set; }
public ContentPackage ContentPackage { get; private set; }
public XDocument XDocument { get; private set; }
public static IEnumerable<string> ConfigFilePaths => Prefabs.Select(p => p.FilePath);
public static IEnumerable<XDocument> ConfigFiles => Prefabs.Select(p => p.XDocument);
public const string HumanSpeciesName = "human";
public static string HumanConfigFile => FindBySpeciesName(HumanSpeciesName).FilePath;
/// <summary>
/// Searches for a character config file from all currently selected content packages,
/// or from a specific package if the contentPackage parameter is given.
/// </summary>
public static CharacterPrefab FindBySpeciesName(string speciesName)
{
speciesName = speciesName.ToLowerInvariant();
if (!Prefabs.ContainsKey(speciesName)) { return null; }
return Prefabs[speciesName];
}
public static CharacterPrefab FindByFilePath(string filePath)
{
return Prefabs.Find(p => p.FilePath.CleanUpPath() == filePath.CleanUpPath());
}
public static CharacterPrefab Find(Predicate<CharacterPrefab> predicate)
{
return Prefabs.Find(predicate);
}
public static void RemoveByFile(string file)
{
Prefabs.RemoveByFile(file);
}
public static bool LoadFromFile(ContentFile file, bool forceOverride=false)
{
return LoadFromFile(file.Path, file.ContentPackage, forceOverride);
}
public static bool LoadFromFile(string filePath, ContentPackage contentPackage, bool forceOverride=false)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null)
{
DebugConsole.ThrowError($"Loading character file failed: {filePath}");
return false;
}
if (Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.FilePath == filePath)))
{
DebugConsole.ThrowError($"Duplicate path: {filePath}");
return false;
}
XElement mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
var name = mainElement.GetAttributeString("name", null);
if (name != null)
{
DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
name = mainElement.GetAttributeString("speciesname", string.Empty);
}
if (string.IsNullOrWhiteSpace(name))
{
DebugConsole.ThrowError($"No species name defined for: {filePath}");
return false;
}
var identifier = name.ToLowerInvariant();
Prefabs.Add(new CharacterPrefab
{
Name = name,
OriginalName = name,
Identifier = identifier,
FilePath = filePath,
ContentPackage = contentPackage,
XDocument = doc
}, forceOverride || doc.Root.IsOverride());
return true;
}
public static void LoadAll()
{
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Character))
{
LoadFromFile(file);
}
}
}
}
@@ -145,7 +145,7 @@ namespace Barotrauma
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) return 0.0f;
if (afflictionId != currentEffect.ResistanceFor) return 0.0f;
if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinResistance,
@@ -106,6 +106,9 @@ namespace Barotrauma
character.NeedsAir = false;
character.SetStun(0.5f);
}
#if CLIENT
character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = true;
#endif
}
}
@@ -116,6 +119,9 @@ namespace Barotrauma
{
huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
huskAppendage = null;
#if CLIENT
character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = false;
#endif
}
}
@@ -150,15 +156,15 @@ namespace Barotrauma
Entity.Spawner.AddToRemoveQueue(character);
string speciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
string configFile = Character.GetConfigFilePath(speciesName);
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (string.IsNullOrEmpty(configFile))
if (prefab == null)
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.");
yield return CoroutineStatus.Success;
}
var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true, ragdoll: character.AnimController.RagdollParams);
var husk = Character.Create(speciesName, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true, ragdoll: character.AnimController.RagdollParams);
foreach (Limb limb in husk.AnimController.Limbs)
{
@@ -204,21 +210,21 @@ namespace Barotrauma
}
string nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
string huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
string filePath = Character.GetConfigFilePath(huskedSpeciesName);
if (!Character.TryGetConfigFile(filePath, out XDocument huskDoc))
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (huskPrefab?.XDocument == null)
{
DebugConsole.ThrowError($"Error in '{filePath}': Failed to load the config file for the husk infected species with the species name '{huskedSpeciesName}'!");
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!");
return appendage;
}
var mainElement = huskDoc.Root.IsOverride() ? huskDoc.Root.FirstElement() : huskDoc.Root;
var mainElement = huskPrefab.XDocument.Root.IsOverride() ? huskPrefab.XDocument.Root.FirstElement() : huskPrefab.XDocument.Root;
var element = appendageDefinition;
if (element == null)
{
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier));
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
}
if (element == null)
{
DebugConsole.ThrowError($"Error in '{filePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!");
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!");
return appendage;
}
string pathToAppendage = element.GetAttributeString("path", string.Empty);
@@ -4,11 +4,13 @@ using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using System.Linq;
using System.Security.Cryptography;
namespace Barotrauma
{
public static class CPRSettings
static class CPRSettings
{
public static string FilePath { get; private set; }
public static bool IsLoaded { get; private set; }
public static float ReviveChancePerSkill { get; private set; }
public static float ReviveChanceExponent { get; private set; }
@@ -20,7 +22,19 @@ namespace Barotrauma
public static float DamageSkillThreshold { get; private set; }
public static float DamageSkillMultiplier { get; private set; }
public static void Load(XElement element)
private static string insufficientSkillAfflictionIdentifier { get; set; }
public static AfflictionPrefab InsufficientSkillAffliction
{
get
{
return
AfflictionPrefab.Prefabs.ContainsKey(insufficientSkillAfflictionIdentifier) ?
AfflictionPrefab.Prefabs[insufficientSkillAfflictionIdentifier] :
AfflictionPrefab.InternalDamage;
}
}
public static void Load(XElement element, string filePath)
{
ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f);
ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f);
@@ -33,13 +47,23 @@ namespace Barotrauma
DamageSkillThreshold = MathHelper.Clamp(element.GetAttributeFloat("damageskillthreshold", 40.0f), 0.0f, 100.0f);
DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f);
insufficientSkillAfflictionIdentifier = element.GetAttributeString("insufficientskillaffliction", "");
IsLoaded = true;
FilePath = filePath;
}
public static void Unload()
{
IsLoaded = false;
FilePath = null;
}
}
class AfflictionPrefabHusk : AfflictionPrefab
{
public AfflictionPrefabHusk(XElement element, Type type = null) : base(element, type)
public AfflictionPrefabHusk(XElement element, string filePath, Type type = null) : base(element, filePath, type)
{
HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant();
if (HuskedSpeciesName == null)
@@ -79,7 +103,7 @@ namespace Barotrauma
public const string Tag = "[speciesname]";
}
class AfflictionPrefab
class AfflictionPrefab : IPrefab, IDisposable
{
public class Effect
{
@@ -144,8 +168,8 @@ namespace Barotrauma
MaxSpeedMultiplier = element.GetAttributeFloat("maxspeedmultiplier", 1.0f);
MaxSpeedMultiplier = Math.Max(MinSpeedMultiplier, MaxSpeedMultiplier);
MinBuffMultiplier = element.GetAttributeFloat("minmultiplier", 1.0f);
MaxBuffMultiplier = element.GetAttributeFloat("maxmultiplier", 1.0f);
MinBuffMultiplier = element.GetAttributeFloat("minbuffmultiplier", 1.0f);
MaxBuffMultiplier = element.GetAttributeFloat("maxbuffmultiplier", 1.0f);
MaxBuffMultiplier = Math.Max(MinBuffMultiplier, MaxBuffMultiplier);
DialogFlag = element.GetAttributeString("dialogflag", "");
@@ -172,10 +196,36 @@ namespace Barotrauma
public static AfflictionPrefab Pressure;
public static AfflictionPrefab Stun;
public static List<AfflictionPrefab> List = new List<AfflictionPrefab>();
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
//Arbitrary string that is used to identify the type of the affliction.
//Afflictions with the same type stack up, and items may be configured to cure specific types of afflictions.
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public static IEnumerable<AfflictionPrefab> List
{
get
{
foreach (var prefab in Prefabs)
{
yield return prefab;
}
}
}
public string FilePath { get; private set; }
/// <summary>
/// Unique identifier that's generated by hashing the prefab's string identifier.
/// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer.
/// </summary>
public uint UIntIdentifier;
// Arbitrary string that is used to identify the type of the affliction.
public readonly string AfflictionType;
//Does the affliction affect a specific limb or the whole character
@@ -185,7 +235,9 @@ namespace Barotrauma
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public readonly string Identifier;
public string Identifier { get; private set; }
public string OriginalName { get { return Identifier; } }
public ContentPackage ContentPackage { get; private set; }
public readonly string Name, Description;
public readonly bool IsBuff;
@@ -211,166 +263,237 @@ namespace Barotrauma
public readonly string AchievementOnRemoved;
public readonly Sprite Icon;
public readonly Color IconColor;
public readonly Color[] IconColors;
private List<Effect> effects = new List<Effect>();
private Dictionary<string, float> treatmentSuitability = new Dictionary<string, float>();
private readonly string typeName;
private readonly ConstructorInfo constructor;
public Dictionary<string, float> TreatmentSuitability
public IEnumerable<KeyValuePair<string, float>> TreatmentSuitability
{
get { return treatmentSuitability; }
get
{
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType));
if (suitability > 0.0f)
{
yield return new KeyValuePair<string, float>(itemPrefab.Identifier, suitability);
}
}
}
}
public static void LoadAll(IEnumerable<string> filePaths)
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (string filePath in filePaths)
CPRSettings.Unload();
InternalDamage = null;
Bleeding = null;
Burn = null;
OxygenLow = null;
Bloodloss = null;
Pressure = null;
Stun = null;
#if CLIENT
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = null;
CharacterHealth.DamageOverlayFile = string.Empty;
#endif
var prevPrefabs = Prefabs.ToList();
foreach (var prefab in prevPrefabs)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { continue; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
prefab.Dispose();
}
System.Diagnostics.Debug.Assert(Prefabs.Count() == 0, "All previous AfflictionPrefabs were not removed in AfflictionPrefab.LoadAll");
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
if (InternalDamage == null) { DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs."); }
if (Bleeding == null) { DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs."); }
if (Burn == null) { DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs."); }
if (OxygenLow == null) { DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs."); }
if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); }
if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); }
if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); }
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one.");
}
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase) &&
!elementName.Equals("damageoverlay", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one.");
}
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase) &&
!elementName.Equals("damageoverlay", StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrWhiteSpace(identifier))
{
if (string.IsNullOrWhiteSpace(identifier))
DebugConsole.ThrowError($"No identifier defined for the affliction '{elementName}' in file '{file.Path}'");
continue;
}
if (Prefabs.ContainsKey(identifier))
{
if (isOverride)
{
DebugConsole.ThrowError($"No identifier defined for the affliction '{elementName}' in file '{filePath}'");
DebugConsole.NewMessage($"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Duplicate affliction: '{identifier}' defined in {elementName} of '{file.Path}'");
continue;
}
var duplicate = List.FirstOrDefault(a => a.Identifier == identifier);
if (duplicate != null)
}
}
string type = sourceElement.GetAttributeString("type", "");
switch (sourceElement.Name.ToString().ToLowerInvariant())
{
case "cprsettings":
type = "cprsettings";
break;
case "damageoverlay":
type = "damageoverlay";
break;
}
AfflictionPrefab prefab = null;
switch (type)
{
case "damageoverlay":
#if CLIENT
if (CharacterHealth.DamageOverlay != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{filePath}'", Color.Yellow);
List.Remove(duplicate);
DebugConsole.NewMessage($"Overriding damage overlay with '{file.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Duplicate affliction: '{identifier}' defined in {elementName} of '{filePath}'");
continue;
DebugConsole.ThrowError($"Error in '{file.Path}': damage overlay already loaded. Add <override></override> tags as the parent of the custom damage overlay sprite to allow overriding the vanilla one.");
break;
}
}
}
string type = sourceElement.GetAttributeString("type", "");
switch (sourceElement.Name.ToString().ToLowerInvariant())
{
case "cprsettings":
type = "cprsettings";
break;
case "damageoverlay":
type = "damageoverlay";
break;
}
AfflictionPrefab prefab = null;
switch (type)
{
case "damageoverlay":
#if CLIENT
if (CharacterHealth.DamageOverlay != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding damage overlay with '{filePath}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{filePath}': damage overlay already loaded. Add <override></override> tags as the parent of the custom damage overlay sprite to allow overriding the vanilla one.");
break;
}
}
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = new Sprite(element);
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = new Sprite(element);
CharacterHealth.DamageOverlayFile = file.Path;
#endif
break;
case "bleeding":
prefab = new AfflictionPrefab(sourceElement, typeof(AfflictionBleeding));
break;
case "huskinfection":
prefab = new AfflictionPrefabHusk(sourceElement, typeof(AfflictionHusk));
break;
case "cprsettings":
if (CPRSettings.IsLoaded)
break;
case "bleeding":
prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(AfflictionBleeding));
break;
case "huskinfection":
prefab = new AfflictionPrefabHusk(sourceElement, file.Path, typeof(AfflictionHusk));
break;
case "cprsettings":
if (CPRSettings.IsLoaded)
{
if (isOverride)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding the CPR settings with '{filePath}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{filePath}': CPR settings already loaded. Add <override></override> tags as the parent of the custom CPRSettings to allow overriding the vanilla values.");
break;
}
DebugConsole.NewMessage($"Overriding the CPR settings with '{file.Path}'", Color.Yellow);
}
CPRSettings.Load(sourceElement);
break;
case "damage":
case "burn":
case "oxygenlow":
case "bloodloss":
case "stun":
case "pressure":
case "internaldamage":
prefab = new AfflictionPrefab(sourceElement, typeof(Affliction));
break;
default:
prefab = new AfflictionPrefab(sourceElement);
break;
}
switch (identifier)
{
case "internaldamage":
InternalDamage = prefab;
break;
case "bleeding":
Bleeding = prefab;
break;
case "burn":
Burn = prefab;
break;
case "oxygenlow":
OxygenLow = prefab;
break;
case "bloodloss":
Bloodloss = prefab;
break;
case "pressure":
Pressure = prefab;
break;
case "stun":
Stun = prefab;
break;
}
if (prefab != null) { List.Add(prefab); }
else
{
DebugConsole.ThrowError($"Error in '{file.Path}': CPR settings already loaded. Add <override></override> tags as the parent of the custom CPRSettings to allow overriding the vanilla values.");
break;
}
}
CPRSettings.Load(sourceElement, file.Path);
break;
case "damage":
case "burn":
case "oxygenlow":
case "bloodloss":
case "stun":
case "pressure":
case "internaldamage":
prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(Affliction))
{
ContentPackage = file.ContentPackage
};
break;
default:
prefab = new AfflictionPrefab(sourceElement, file.Path)
{
ContentPackage = file.ContentPackage
};
break;
}
switch (identifier)
{
case "internaldamage":
InternalDamage = prefab;
break;
case "bleeding":
Bleeding = prefab;
break;
case "burn":
Burn = prefab;
break;
case "oxygenlow":
OxygenLow = prefab;
break;
case "bloodloss":
Bloodloss = prefab;
break;
case "pressure":
Pressure = prefab;
break;
case "stun":
Stun = prefab;
break;
}
if (prefab != null)
{
Prefabs.Add(prefab, isOverride);
}
}
if (InternalDamage == null) DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs.");
if (Bleeding == null) DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs.");
if (Burn == null) DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs.");
if (OxygenLow == null) DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs.");
if (Bloodloss == null) DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs.");
if (Pressure == null) DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs.");
if (Stun == null) DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs.");
using MD5 md5 = MD5.Create();
foreach (AfflictionPrefab prefab in Prefabs)
{
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
if (collision != null)
{
DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Afflictions: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")");
collision.UIntIdentifier++;
}
}
}
public AfflictionPrefab(XElement element, Type type = null)
public static void RemoveByFile(string filePath)
{
if (CPRSettings.FilePath == filePath) { CPRSettings.Unload(); }
#if CLIENT
if (CharacterHealth.DamageOverlayFile == filePath)
{
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = null;
}
#endif
Prefabs.RemoveByFile(filePath);
}
public AfflictionPrefab(XElement element, string filePath, Type type = null)
{
FilePath = filePath;
typeName = type == null ? element.Name.ToString() : type.Name;
if (typeName == "InternalDamage" && type == null)
{
@@ -408,7 +531,7 @@ namespace Barotrauma
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
IconColors = element.GetAttributeColorArray("iconcolors", null);
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
foreach (XElement subElement in element.Elements())
@@ -417,7 +540,6 @@ namespace Barotrauma
{
case "icon":
Icon = new Sprite(subElement);
IconColor = subElement.GetAttributeColor("color", Color.White);
break;
case "effect":
effects.Add(new Effect(subElement, Name));
@@ -491,11 +613,11 @@ namespace Barotrauma
public float GetTreatmentSuitability(Item item)
{
if (item == null || !treatmentSuitability.ContainsKey(item.Prefab.Identifier.ToLowerInvariant()))
if (item == null)
{
return 0.0f;
}
return treatmentSuitability[item.Prefab.Identifier.ToLowerInvariant()];
return Math.Max(item.Prefab.GetTreatmentSuitability(Identifier), item.Prefab.GetTreatmentSuitability(AfflictionType));
}
}
}
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
@@ -44,7 +45,7 @@ namespace Barotrauma
}
else
{
var invertControlsAffliction = AfflictionPrefab.List.Find(ap => ap.Identifier == "invertcontrols");
var invertControlsAffliction = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == "invertcontrols");
characterHealth.ApplyAffliction(null, new Affliction(invertControlsAffliction, 5.0f));
}
}
@@ -13,6 +13,7 @@ namespace Barotrauma
class LimbHealth
{
public Sprite IndicatorSprite;
public Sprite HighlightSprite;
public Rectangle HighlightArea;
@@ -44,6 +45,9 @@ namespace Barotrauma
IndicatorSprite = new Sprite(subElement);
HighlightArea = subElement.GetAttributeRect("highlightarea", new Rectangle(0, 0, (int)IndicatorSprite.size.X, (int)IndicatorSprite.size.Y));
break;
case "highlightsprite":
HighlightSprite = new Sprite(subElement);
break;
case "vitalitymultiplier":
if (subElement.Attribute("name") != null)
{
@@ -212,7 +216,7 @@ namespace Barotrauma
limbHealths.Clear();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "limb") continue;
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
limbHealths.Add(new LimbHealth(subElement, this));
}
if (limbHealths.Count == 0)
@@ -404,11 +408,10 @@ namespace Barotrauma
return resistance;
}
private List<Affliction> matchingAfflictions = new List<Affliction>();
public void ReduceAffliction(Limb targetLimb, string affliction, float amount)
{
affliction = affliction.ToLowerInvariant();
List<Affliction> matchingAfflictions = new List<Affliction>(afflictions);
matchingAfflictions.Clear();
if (targetLimb != null)
{
@@ -422,8 +425,8 @@ namespace Barotrauma
}
}
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier.ToLowerInvariant() != affliction &&
a.Prefab.AfflictionType.ToLowerInvariant() != affliction);
!a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) &&
!a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase));
if (matchingAfflictions.Count == 0) return;
@@ -619,7 +622,7 @@ namespace Barotrauma
{
UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime);
}
Character.SpeedMultiplier = affliction.GetSpeedMultiplier();
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
}
@@ -638,7 +641,7 @@ namespace Barotrauma
var affliction = afflictions[i];
affliction.Update(this, null, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
Character.SpeedMultiplier = affliction.GetSpeedMultiplier();
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
UpdateLimbAfflictionOverlays();
@@ -687,13 +690,15 @@ namespace Barotrauma
foreach (Affliction affliction in limbHealth.Afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier.ToLowerInvariant()))
string identifier = affliction.Prefab.Identifier.ToLowerInvariant();
string type = affliction.Prefab.AfflictionType.ToLowerInvariant();
if (limbHealth.VitalityMultipliers.ContainsKey(identifier))
{
vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier.ToLowerInvariant()];
vitalityDecrease *= limbHealth.VitalityMultipliers[identifier];
}
if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType.ToLowerInvariant()))
if (limbHealth.VitalityTypeMultipliers.ContainsKey(type))
{
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()];
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type];
}
vitalityDecrease *= damageResistanceMultiplier;
Vitality -= vitalityDecrease;
@@ -787,8 +792,7 @@ namespace Barotrauma
/// </summary>
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
/// <param name="normalize">If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable.</param>
/// <param name="randomization">Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random)</param>
/// <param name="randomization">Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random)</param>
public void GetSuitableTreatments(Dictionary<string, float> treatmentSuitability, bool normalize, float randomization = 0.0f)
{
//key = item identifier
@@ -836,7 +840,7 @@ namespace Barotrauma
msg.Write((byte)activeAfflictions.Count);
foreach (Affliction affliction in activeAfflictions)
{
msg.WriteRangedInteger(AfflictionPrefab.List.IndexOf(affliction.Prefab), 0, AfflictionPrefab.List.Count - 1);
msg.Write(affliction.Prefab.UIntIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
@@ -856,7 +860,7 @@ namespace Barotrauma
foreach (var limbAffliction in limbAfflictions)
{
msg.WriteRangedInteger(limbHealths.IndexOf(limbAffliction.First), 0, limbHealths.Count - 1);
msg.WriteRangedInteger(AfflictionPrefab.List.IndexOf(limbAffliction.Second.Prefab), 0, AfflictionPrefab.List.Count - 1);
msg.Write(limbAffliction.Second.Prefab.UIntIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength),
0.0f, limbAffliction.Second.Prefab.MaxStrength, 8);
@@ -869,5 +873,11 @@ namespace Barotrauma
}
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions) =>
afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
}
}
@@ -97,14 +97,33 @@ namespace Barotrauma
parsedAfflictionIdentifiers = splitValue;
}
public bool MatchesAffliction(Affliction affliction)
public bool MatchesAfflictionIdentifier(string identifier)
{
//if no identifiers have been defined, the damage modifier affects all afflictions
if (AfflictionIdentifiers.Length == 0) { return true; }
return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase));
}
public bool MatchesAfflictionType(string type)
{
//if no types have been defined, the damage modifier affects all afflictions
if (AfflictionTypes.Length == 0) { return true; }
return parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Returns true if the type or the identifier matches the defined types/identifiers.
/// </summary>
public bool MatchesAffliction(string identifier, string type)
{
//if no identifiers or types have been defined, the damage modifier affects all afflictions
if (AfflictionIdentifiers.Length == 0 && AfflictionTypes.Length == 0) { return true; }
return parsedAfflictionIdentifiers.Any(id => id.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase))
|| parsedAfflictionTypes.Any(t => t.Equals(affliction.Prefab.AfflictionType, StringComparison.OrdinalIgnoreCase));
return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase))
|| parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase));
}
public bool MatchesAffliction(Affliction affliction) => MatchesAffliction(affliction.Identifier, affliction.Prefab.AfflictionType);
public void Serialize(XElement element)
{
if (element == null) { return; }
@@ -25,11 +25,6 @@ namespace Barotrauma
{
get { return prefab; }
}
public XElement SpawnItems
{
get { return prefab.Items; }
}
public List<Skill> Skills
{
@@ -53,16 +48,21 @@ namespace Barotrauma
public Job(XElement element)
{
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
if (!JobPrefab.List.TryGetValue(identifier, out JobPrefab p))
JobPrefab p = null;
if (!JobPrefab.Prefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
p = JobPrefab.Random();
}
else
{
p = JobPrefab.Prefabs[identifier];
}
prefab = p;
skills = new Dictionary<string, Skill>();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "skill") { continue; }
if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
skills.Add(
@@ -70,8 +70,13 @@ namespace Barotrauma
new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)));
}
}
public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced) => new Job(JobPrefab.Random(randSync));
public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
var prefab = JobPrefab.Random(randSync);
var variant = Rand.Range(0, prefab.Variants, randSync);
return new Job(prefab, variant);
}
public float GetSkillLevel(string skillIdentifier)
{
@@ -96,9 +101,9 @@ namespace Barotrauma
public void GiveJobItems(Character character, WayPoint spawnPoint = null)
{
if (SpawnItems == null) return;
if (!prefab.ItemSets.TryGetValue(Variant, out var spawnItems)) { return; }
foreach (XElement itemElement in SpawnItems.Elements())
foreach (XElement itemElement in spawnItems.GetChildElements("Item"))
{
InitializeJobItem(character, itemElement, spawnPoint);
}
@@ -0,0 +1,292 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System;
using System.Linq;
namespace Barotrauma
{
public class AutonomousObjective
{
public string identifier;
public string option;
public float priorityModifier;
public AutonomousObjective(XElement element)
{
identifier = element.GetAttributeString("identifier", null);
//backwards compatibility
if (string.IsNullOrEmpty(identifier))
{
identifier = element.GetAttributeString("aitag", null);
}
option = element.GetAttributeString("option", null);
priorityModifier = element.GetAttributeFloat("prioritymodifier", 1);
priorityModifier = MathHelper.Max(priorityModifier, 0);
}
}
partial class JobPrefab : IPrefab, IDisposable
{
public static readonly PrefabCollection<JobPrefab> Prefabs = new PrefabCollection<JobPrefab>();
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public static XElement NoJobElement;
public static JobPrefab Get(string identifier)
{
if (Prefabs == null)
{
DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded.");
return null;
}
if (Prefabs.ContainsKey(identifier))
{
return Prefabs[identifier];
}
else
{
DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier);
return null;
}
}
public readonly Dictionary<int, XElement> ItemSets = new Dictionary<int, XElement>();
public readonly Dictionary<int, List<string>> ItemIdentifiers = new Dictionary<int, List<string>>();
public readonly Dictionary<int, Dictionary<string, bool>> ShowItemPreview = new Dictionary<int, Dictionary<string, bool>>();
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutomaticOrders = new List<AutonomousObjective>();
public readonly List<string> AppropriateOrders = new List<string>();
[Serialize("1,1,1,1", false)]
public Color UIColor
{
get;
private set;
}
[Serialize("notfound", false)]
public string Identifier
{
get;
private set;
}
[Serialize("notfound", false)]
public string Name
{
get;
private set;
}
public string OriginalName { get { return Identifier; } }
public ContentPackage ContentPackage { get; private set; }
[Serialize("", false)]
public string Description
{
get;
private set;
}
[Serialize(false, false)]
public bool OnlyJobSpecificDialog
{
get;
private set;
}
//the number of these characters in the crew the player starts with in the single player campaign
[Serialize(0, false)]
public int InitialCount
{
get;
private set;
}
//if set to true, a client that has chosen this as their preferred job will get it no matter what
[Serialize(false, false)]
public bool AllowAlways
{
get;
private set;
}
//how many crew members can have the job (only one captain etc)
[Serialize(100, false)]
public int MaxNumber
{
get;
private set;
}
//how many crew members are REQUIRED to have the job
//(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference)
[Serialize(0, false)]
public int MinNumber
{
get;
private set;
}
[Serialize(0.0f, false)]
public float MinKarma
{
get;
private set;
}
[Serialize(10.0f, false)]
public float Commonness
{
get;
private set;
}
//how much the vitality of the character is increased/reduced from the default value
[Serialize(0.0f, false)]
public float VitalityModifier
{
get;
private set;
}
public Sprite Icon;
public string FilePath { get; private set; }
public XElement Element { get; private set; }
public XElement ClothingElement { get; private set; }
public int Variants { get; private set; }
public JobPrefab(XElement element, string filePath)
{
FilePath = filePath;
SerializableProperty.DeserializeProperties(this, element);
Name = TextManager.Get("JobName." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier);
Identifier = Identifier.ToLowerInvariant();
Element = element;
int variant = 0;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "itemset":
ItemSets.Add(variant, subElement);
ItemIdentifiers[variant] = new List<string>();
ShowItemPreview[variant] = new Dictionary<string, bool>();
loadItemIdentifiers(subElement, variant);
variant++;
break;
case "skills":
foreach (XElement skillElement in subElement.Elements())
{
Skills.Add(new SkillPrefab(skillElement));
}
break;
case "autonomousobjectives":
subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order)));
break;
case "appropriateobjectives":
case "appropriateorders":
subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeString("identifier", "").ToLowerInvariant()));
break;
case "jobicon":
Icon = new Sprite(subElement.FirstElement());
break;
}
}
void loadItemIdentifiers(XElement parentElement, int variant)
{
foreach (XElement itemElement in parentElement.GetChildElements("Item"))
{
if (itemElement.Element("name") != null)
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.");
continue;
}
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
if (string.IsNullOrWhiteSpace(itemIdentifier))
{
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier.");
}
else
{
ItemIdentifiers[variant].Add(itemIdentifier);
ShowItemPreview[variant][itemIdentifier] = itemElement.GetAttributeBool("showpreview", true);
}
loadItemIdentifiers(itemElement, variant);
}
}
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X));
ClothingElement = element.GetChildElement("PortraitClothing");
}
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync);
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
}
public static void LoadFromFile(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
DebugConsole.ThrowError($"Error in '{file.Path}': Cannot override all job prefabs, because many of them are required by the main game! Please try overriding jobs one by one.");
}
foreach (XElement element in mainElement.Elements())
{
if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; }
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement(), file.Path)
{
ContentPackage = file.ContentPackage
};
Prefabs.Add(job, true);
}
else
{
var job = new JobPrefab(element, file.Path)
{
ContentPackage = file.ContentPackage
};
Prefabs.Add(job, false);
}
}
NoJobElement = NoJobElement ?? mainElement.Element("NoJob");
NoJobElement = NoJobElement ?? mainElement.Element("nojob");
}
public static void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
}
public readonly Attack attack;
private List<DamageModifier> damageModifiers;
public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>();
private Direction dir;
@@ -128,6 +128,7 @@ namespace Barotrauma
public float Scale => Params.Ragdoll.LimbScale;
public float AttackPriority => Params.AttackPriority;
public bool DoesFlip => Params.Flip;
public float SteerForce => Params.SteerForce;
public Vector2 DebugTargetPos;
@@ -138,7 +139,12 @@ namespace Barotrauma
get { return isSevered; }
set
{
if (isSevered == value) { return; }
isSevered = value;
if (isSevered)
{
ragdoll.SubtractMass(this);
}
if (!isSevered) severedFadeOutTimer = 0.0f;
#if CLIENT
if (isSevered) damageOverlayStrength = 100.0f;
@@ -160,12 +166,36 @@ namespace Barotrauma
public Vector2 SimPosition
{
get { return body.SimPosition; }
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return Vector2.Zero;
}
return body.SimPosition;
}
}
public float Rotation
{
get { return body.Rotation; }
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return 0.0f;
}
return body.Rotation;
}
}
//where an animcontroller is trying to pull the limb, only used for debug visualization
@@ -173,14 +203,38 @@ namespace Barotrauma
public float Mass
{
get { return body.Mass; }
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return 1.0f;
}
return body.Mass;
}
}
public bool Disabled { get; set; }
public Vector2 LinearVelocity
{
get { return body.LinearVelocity; }
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
return Vector2.Zero;
}
return body.LinearVelocity;
}
}
public float Dir
@@ -282,6 +336,12 @@ namespace Barotrauma
get { return pullJoint.LocalAnchorA; }
}
public bool Removed
{
get;
private set;
}
public string Name => Params.Name;
public Dictionary<string, SerializableProperty> SerializableProperties
@@ -320,14 +380,12 @@ namespace Barotrauma
MaxForce = 1000 * Mass
};
GameMain.World.AddJoint(pullJoint);
GameMain.World.Add(pullJoint);
var element = limbParams.Element;
body.BodyType = BodyType.Dynamic;
damageModifiers = new List<DamageModifier>();
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -352,7 +410,7 @@ namespace Barotrauma
}
break;
case "damagemodifier":
damageModifiers.Add(new DamageModifier(subElement, character.Name));
DamageModifiers.Add(new DamageModifier(subElement, character.Name));
break;
}
}
@@ -398,7 +456,7 @@ namespace Barotrauma
var afflictionsCopy = afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.Probability).ToList();
for (int i = 0; i < afflictionsCopy.Count; i++)
{
foreach (DamageModifier damageModifier in damageModifiers)
foreach (DamageModifier damageModifier in DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue;
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
@@ -478,7 +536,6 @@ namespace Barotrauma
private readonly List<Body> contactBodies = new List<Body>();
private List<Body> ignoredBodies;
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
/// </summary>
@@ -498,20 +555,11 @@ namespace Barotrauma
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
if (ignoredBodies == null)
{
ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
}
structureBody = Submarine.PickBody(
SimPosition, attackSimPos,
ignoredBodies, Physics.CollisionWall);
if (damageTarget is Item)
structureBody = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
{
// If the attack is aimed to an item and hits an item, it's successful.
// Ignore blocking on items, because it causes cases where a Mudraptor cannot hit the hatch, for example.
// Ignore blocking checks on doors, because it causes cases where a Mudraptor cannot hit the hatch, for example.
wasHit = true;
}
else if (damageTarget is Structure wall && structureBody != null &&
@@ -609,7 +657,7 @@ namespace Barotrauma
bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f;
if (applyForces)
{
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
{
foreach (int limbIndex in attack.ForceOnLimbIndices)
@@ -617,6 +665,7 @@ namespace Barotrauma
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue;
Limb limb = character.AnimController.Limbs[limbIndex];
limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque);
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
@@ -624,6 +673,7 @@ namespace Barotrauma
}
else
{
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
body.ApplyLinearImpulse(
Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition),
@@ -668,7 +718,7 @@ namespace Barotrauma
KinematicBodyB = true,
CollideConnected = false
};
GameMain.World.AddJoint(colliderJoint);
GameMain.World.Add(colliderJoint);
}
attachJoint = new WeldJoint(body.FarseerBody, target, from, to, true)
@@ -678,17 +728,17 @@ namespace Barotrauma
KinematicBodyB = true,
CollideConnected = false
};
GameMain.World.AddJoint(attachJoint);
GameMain.World.Add(attachJoint);
}
public void Release()
{
if (!IsStuck) { return; }
GameMain.World.RemoveJoint(attachJoint);
GameMain.World.Remove(attachJoint);
attachJoint = null;
if (colliderJoint != null)
{
GameMain.World.RemoveJoint(colliderJoint);
GameMain.World.Remove(colliderJoint);
colliderJoint = null;
}
}
@@ -699,6 +749,7 @@ namespace Barotrauma
body = null;
Release();
RemoveProjSpecific();
Removed = true;
}
partial void RemoveProjSpecific();
@@ -14,6 +14,8 @@ namespace Barotrauma
get { return list; }
}
public readonly string FilePath;
public readonly string Name;
public readonly List<string> AllowedDialogTags;
@@ -24,8 +26,9 @@ namespace Barotrauma
get { return commonness; }
}
public NPCPersonalityTrait(XElement element)
public NPCPersonalityTrait(XElement element, string filePath)
{
FilePath = filePath;
Name = element.GetAttributeString("name", "");
AllowedDialogTags = new List<string>(element.GetAttributeStringArray("alloweddialogtags", new string[0]));
commonness = element.GetAttributeFloat("commonness", 1.0f);
@@ -113,16 +113,21 @@ namespace Barotrauma
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
string configFilePath = Character.GetConfigFilePath(speciesName, contentPackage);
if (!Character.TryGetConfigFile(configFilePath, out XDocument configFile))
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab?.XDocument == null)
{
DebugConsole.ThrowError($"Failed to load config file: {configFilePath} for '{speciesName}'");
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
return string.Empty;
}
var folder = configFile.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
return GetFolder(prefab.XDocument, prefab.FilePath);
}
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(configFilePath), "Animations");
folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations");
}
return folder;
}
@@ -193,7 +198,7 @@ namespace Barotrauma
}
else
{
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
@@ -40,9 +40,18 @@ namespace Barotrauma
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Noise { get; set; }
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Visibility { get; set; }
[Serialize("blood", true), Editable]
public string BloodDecal { get; private set; }
[Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
[Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)]
public float PathFinderPriority { get; set; }
public readonly string File;
public readonly List<SubParam> SubParams = new List<SubParam>();
@@ -189,7 +198,7 @@ namespace Barotrauma
}
if (space > 0)
{
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, (int)(space * GUI.yScale)), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
{
CanBeFocused = false
};
@@ -435,8 +444,8 @@ namespace Barotrauma
[Serialize(0f, true, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)]
public float FleeHealthThreshold { get; private set; }
[Serialize(false, true, description: "Does the character attack ONLY when provoked?"), Editable()]
public bool AttackOnlyWhenProvoked { get; private set; }
[Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()]
public bool AttackWhenProvoked { get; private set; }
[Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
public bool AvoidGunfire { get; private set; }
@@ -458,7 +467,7 @@ namespace Barotrauma
private bool TryAddTarget(XElement targetElement, out TargetParams target)
{
string tag = targetElement.GetAttributeString("tag", null);
if (!CheckTag(tag))
if (HasTag(tag))
{
target = null;
DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
@@ -481,22 +490,25 @@ namespace Barotrauma
if (TryAddTarget(element, out targetParams))
{
Element.Add(element);
return true;
}
else
{
return false;
}
return targetParams != null;
}
private bool CheckTag(string tag)
public bool HasTag(string tag)
{
if (tag == null) { return false; }
tag = tag.ToLowerInvariant();
return targets.None(t => t.Tag == tag);
return targets.Any(t => t.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase));
}
public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets);
public bool TryGetTarget(string targetTag, out TargetParams target)
{
target = targets.FirstOrDefault(t => t.Tag == targetTag);
target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetTag, StringComparison.OrdinalIgnoreCase));
return target != null;
}
@@ -523,9 +535,12 @@ namespace Barotrauma
[Serialize(AIState.Idle, true), Editable]
public AIState State { get; set; }
[Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f)]
[Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)]
public float Priority { get; set; }
[Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. Eg. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
public float ReactDistance { get; set; }
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }
@@ -94,16 +94,21 @@ namespace Barotrauma
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
string configFilePath = Character.GetConfigFilePath(speciesName, contentPackage);
if (!Character.TryGetConfigFile(configFilePath, out XDocument configFile))
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage));
if (prefab?.XDocument == null)
{
DebugConsole.ThrowError($"Failed to load config file: {configFilePath} for '{speciesName}'");
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})");
return string.Empty;
}
var folder = configFile.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
return GetFolder(prefab.XDocument, prefab.FilePath);
}
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(configFilePath), "Ragdolls") + Path.DirectorySeparatorChar;
folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar;
}
return folder;
}
@@ -144,7 +149,7 @@ namespace Barotrauma
}
else
{
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.");
@@ -561,13 +566,13 @@ namespace Barotrauma
[Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
[Serialize(10f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float Density { get; set; }
[Serialize(false, true), Editable]
public bool IgnoreCollisions { get; set; }
[Serialize(7f, true), Editable]
[Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable]
public float AngularDamping { get; set; }
[Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()]
@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class SkillSettings : ISerializableEntity
{
public static SkillSettings Current
{
get;
private set;
}
[Serialize(4.0f, true)]
public float SingleRoundSkillGainMultiplier { get; set; }
private float skillIncreasePerRepair;
[Serialize(5.0f, true)]
public float SkillIncreasePerRepair
{
get { return skillIncreasePerRepair * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerRepair = value; }
}
private float skillIncreasePerSabotage;
[Serialize(3.0f, true)]
public float SkillIncreasePerSabotage
{
get { return skillIncreasePerSabotage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSabotage = value; }
}
private float skillIncreasePerCprRevive;
[Serialize(0.5f, true)]
public float SkillIncreasePerCprRevive
{
get { return skillIncreasePerCprRevive * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerCprRevive = value; }
}
private float skillIncreasePerRepairedStructureDamage;
[Serialize(0.005f, true)]
public float SkillIncreasePerRepairedStructureDamage
{
get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerRepairedStructureDamage = value; }
}
private float skillIncreasePerSecondWhenSteering;
[Serialize(0.005f, true)]
public float SkillIncreasePerSecondWhenSteering
{
get { return skillIncreasePerSecondWhenSteering * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSecondWhenSteering = value; }
}
private float skillIncreasePerFabricatorRequiredSkill;
[Serialize(0.5f, true)]
public float SkillIncreasePerFabricatorRequiredSkill
{
get { return skillIncreasePerFabricatorRequiredSkill * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerFabricatorRequiredSkill = value; }
}
private float skillIncreasePerHostileDamage;
[Serialize(0.01f, true)]
public float SkillIncreasePerHostileDamage
{
get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerHostileDamage = value; }
}
private float skillIncreasePerSecondWhenOperatingTurret;
[Serialize(0.001f, true)]
public float SkillIncreasePerSecondWhenOperatingTurret
{
get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSecondWhenOperatingTurret = value; }
}
private float skillIncreasePerFriendlyHealed;
[Serialize(0.001f, true)]
public float SkillIncreasePerFriendlyHealed
{
get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerFriendlyHealed = value; }
}
[Serialize(1.1f, true)]
public float AssistantSkillIncreaseMultiplier
{
get;
set;
}
private SkillSettings(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public string Name => "SkillSettings";
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
set;
}
public static void Load(IEnumerable<ContentFile> files)
{
//reverse order to respect content package load order (last file overrides others)
foreach (ContentFile file in files.Reverse())
{
if (file.Type != ContentType.SkillSettings)
{
throw new ArgumentException();
}
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
Current = new SkillSettings(doc.Root);
break;
}
if (Current == null)
{
DebugConsole.NewMessage("Now skill settings found in the selected content packages. Using default values.");
Current = new SkillSettings(null);
}
}
private float GetCurrentSkillGainMultiplier()
{
if (GameMain.GameSession?.GameMode is CampaignMode)
{
return 1.0f;
}
else
{
return SingleRoundSkillGainMultiplier;
}
}
}
}
@@ -1,5 +1,4 @@
using Facepunch.Steamworks;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -39,7 +38,8 @@ namespace Barotrauma
UIStyle,
TraitorMissions,
EventManagerSettings,
Orders
Orders,
SkillSettings
}
public class ContentPackage
@@ -95,6 +95,8 @@ namespace Barotrauma
get { return corePackageRequiredFiles; }
}
public static bool IngameModSwap = false;
public string Name { get; set; }
public string Path
@@ -117,7 +119,17 @@ namespace Barotrauma
{
get
{
if (md5Hash == null) CalculateHash();
if (md5Hash == null)
{
//TODO: before re-enabling content package hash caching, make sure the hash gets recalculated when any file in the content package changes, not just when the filelist.xml changes.
/*md5Hash = Md5Hash.FetchFromCache(Path);
if (md5Hash == null)
{
CalculateHash();
md5Hash.SaveToCache(Path);
}*/
CalculateHash();
}
return md5Hash;
}
}
@@ -151,6 +163,8 @@ namespace Barotrauma
public ContentPackage(string filePath, string setPath = "")
: this()
{
filePath = filePath.CleanUpPath();
if (!string.IsNullOrEmpty(setPath)) { setPath = setPath.CleanUpPath(); }
XDocument doc = XMLExtensions.TryLoadXml(filePath);
Path = setPath == string.Empty ? filePath : setPath;
@@ -179,7 +193,7 @@ namespace Barotrauma
errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type.");
type = ContentType.None;
}
Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type));
Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this));
}
bool compatible = IsCompatible();
@@ -193,16 +207,16 @@ namespace Barotrauma
}
}
private bool? invalid;
public bool Invalid
private bool? hasErrors;
public bool HasErrors
{
get
{
if (!invalid.HasValue)
if (!hasErrors.HasValue)
{
invalid = !CheckValidity(out _);
hasErrors = !CheckErrors(out _);
}
return invalid.Value;
return hasErrors.Value;
}
}
@@ -211,13 +225,11 @@ namespace Barotrauma
{
get
{
if (errorMessages == null) { CheckValidity(out _); }
if (errorMessages == null) { CheckErrors(out _); }
return errorMessages;
}
}
public bool NeedsRestart;
public override string ToString()
{
return Name;
@@ -260,7 +272,7 @@ namespace Barotrauma
return missingContentTypes.Count == 0;
}
public bool CheckValidity(out List<string> errorMessages)
public bool CheckErrors(out List<string> errorMessages)
{
this.errorMessages = errorMessages = new List<string>();
foreach (ContentFile file in Files)
@@ -290,7 +302,7 @@ namespace Barotrauma
{
errorMessages.Add($"XML File Invalid. PATH: {file.Path}, ERROR: {e.Message}");
#if DEBUG
throw e;
throw;
#endif
}
}
@@ -308,8 +320,8 @@ namespace Barotrauma
VerifyFiles(out List<string> missingFileMessages);
errorMessages.AddRange(missingFileMessages);
invalid = errorMessages.Count > 0;
return !invalid.Value;
hasErrors = errorMessages.Count > 0;
return !hasErrors.Value;
}
/// <summary>
@@ -321,10 +333,10 @@ 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
//TODO: determine executable extension on platform and check for the presence of the executables
if (file.Type == ContentType.Executable) { continue; }
#endif
if (file.Type == ContentType.ServerExecutable) { continue; }
if (!File.Exists(file.Path))
{
errorMessages.Add("File \"" + file.Path + "\" not found.");
@@ -368,7 +380,7 @@ namespace Barotrauma
XDocument doc = new XDocument();
doc.Add(new XElement("contentpackage",
new XAttribute("name", Name),
new XAttribute("path", Path),
new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)),
new XAttribute("corepackage", CorePackage)));
@@ -386,7 +398,7 @@ namespace Barotrauma
foreach (ContentFile file in Files)
{
doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path)));
doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path.CleanUpPathCrossPlatform())));
}
doc.Save(filePath);
@@ -403,7 +415,7 @@ namespace Barotrauma
foreach (ContentFile file in Files)
{
if (!multiplayerIncompatibleContent.Contains(file.Type)) continue;
if (!multiplayerIncompatibleContent.Contains(file.Type)) { continue; }
try
{
@@ -411,7 +423,7 @@ namespace Barotrauma
if (logging)
{
var fileMd5 = new Md5Hash(hash);
DebugConsole.NewMessage(" " + file.Path + ": " + fileMd5.ShortHash);
DebugConsole.NewMessage(" " + file.Path + ": " + fileMd5.Hash);
}
hashes.Add(hash);
}
@@ -431,84 +443,69 @@ namespace Barotrauma
md5Hash = new Md5Hash(bytes);
if (logging)
{
DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.ShortHash);
DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.Hash);
}
}
private byte[] CalculateFileHash(ContentFile file)
{
var md5 = MD5.Create();
List<string> filePaths = new List<string> { file.Path };
List<byte> data = new List<byte>();
switch (file.Type)
using (MD5 md5 = MD5.Create())
{
case ContentType.Character:
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
var rootElement = doc.Root;
var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
var speciesName = element.GetAttributeString("speciesname", element.GetAttributeString("name", ""));
var ragdollFolder = RagdollParams.GetFolder(speciesName);
if (Directory.Exists(ragdollFolder))
{
Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
var animationFolder = AnimationParams.GetFolder(speciesName);
if (Directory.Exists(animationFolder))
{
Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
break;
}
List<string> filePaths = new List<string> { file.Path };
List<byte> data = new List<byte>();
foreach (string filePath in filePaths)
{
if (!File.Exists(filePath)) continue;
using (var stream = File.OpenRead(filePath))
switch (file.Type)
{
byte[] fileData = new byte[stream.Length];
stream.Read(fileData, 0, (int)stream.Length);
if (filePath.EndsWith(".xml", true, System.Globalization.CultureInfo.InvariantCulture))
{
string text = System.Text.Encoding.UTF8.GetString(fileData);
text = text.Replace("\n", "").Replace("\r", "");
fileData = System.Text.Encoding.UTF8.GetBytes(text);
}
data.AddRange(fileData);
case ContentType.Character:
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
var rootElement = doc.Root;
var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
var ragdollFolder = RagdollParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
if (Directory.Exists(ragdollFolder))
{
Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
var animationFolder = AnimationParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
if (Directory.Exists(animationFolder))
{
Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
break;
}
}
return md5.ComputeHash(data.ToArray());
}
public static string GetFileExtension(ContentType contentType)
{
switch (contentType)
{
case ContentType.Executable:
case ContentType.ServerExecutable:
return ".exe";
default:
return ".xml";
if (filePaths.Count > 1)
{
using (MD5 tempMd5 = MD5.Create())
{
filePaths = filePaths.OrderBy(f => ToolBox.StringToUInt32Hash(f.CleanUpPathCrossPlatform(true), tempMd5)).ToList();
}
}
foreach (string filePath in filePaths)
{
if (!File.Exists(filePath)) continue;
using (var stream = File.OpenRead(filePath))
{
byte[] fileData = new byte[stream.Length];
stream.Read(fileData, 0, (int)stream.Length);
if (filePath.EndsWith(".xml", true, System.Globalization.CultureInfo.InvariantCulture))
{
string text = System.Text.Encoding.UTF8.GetString(fileData);
text = text.Replace("\n", "").Replace("\r", "").Replace("\\","/");
fileData = System.Text.Encoding.UTF8.GetBytes(text);
}
data.AddRange(fileData);
}
}
return md5.ComputeHash(data.ToArray());
}
}
public static bool IsModFilePathAllowed(ContentFile contentFile)
{
string path = contentFile.Path;
while (true)
{
string temp = System.IO.Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(temp)) { break; }
path = temp;
}
switch (contentFile.Type)
{
case ContentType.Submarine:
return path == "Submarines" || path == "Mods";
default:
return path == "Mods";
}
return IsModFilePathAllowed(path);
}
/// <summary>
/// Are mods allowed to install a file into the specified path. If a content package XML includes files
@@ -536,9 +533,9 @@ namespace Barotrauma
return contentPackages.SelectMany(f => f.Files).Select(f => f.Path).Where(p => p.EndsWith(".xml"));
}
public static IEnumerable<string> GetFilesOfType(IEnumerable<ContentPackage> contentPackages, ContentType type)
public static IEnumerable<ContentFile> GetFilesOfType(IEnumerable<ContentPackage> contentPackages, ContentType type)
{
return contentPackages.SelectMany(f => f.Files).Where(f => f.Type == type).Select(f => f.Path);
return contentPackages.SelectMany(f => f.Files).Where(f => f.Type == type);
}
public IEnumerable<string> GetFilesOfType(ContentType type)
@@ -581,20 +578,31 @@ namespace Barotrauma
List.Add(new ContentPackage(modFilePath));
}
}
List = List
.OrderByDescending(p => p.CorePackage)
.ThenByDescending(p => GameMain.Config?.SelectedContentPackages.Contains(p))
.ThenBy(p => GameMain.Config?.SelectedContentPackages.IndexOf(p))
.ToList();
}
public static void SortContentPackages()
{
List = List
.OrderByDescending(p => p.CorePackage)
.ThenByDescending(p => GameMain.Config?.SelectedContentPackages.Contains(p))
.ThenBy(p => GameMain.Config?.SelectedContentPackages.IndexOf(p))
.ThenBy(p => List.IndexOf(p))
.ToList();
if (GameMain.Config != null)
{
var sortedSelected = GameMain.Config.SelectedContentPackages
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => List.IndexOf(p))
.ToList();
GameMain.Config.SelectedContentPackages.Clear(); GameMain.Config.SelectedContentPackages.AddRange(sortedSelected);
var reportList = List.Where(p => GameMain.Config.SelectedContentPackages.Contains(p));
DebugConsole.NewMessage($"Content package load order: { new string(reportList.SelectMany(cp => cp.Name + " | ").ToArray()) }");
DebugConsole.NewMessage($"Content package load order: { string.Join(" | ", reportList.Select(cp => cp.Name)) }");
}
}
@@ -602,17 +610,17 @@ namespace Barotrauma
{
try
{
File.Delete(Path);
GameMain.Config.DeselectContentPackage(this);
GameMain.Config.SaveNewPlayerConfig();
List.Remove(this);
File.Delete(Path);
SortContentPackages();
}
catch (IOException e)
catch (Exception e)
{
DebugConsole.ThrowError("Failed to delete content package \"" + Name + "\".", e);
return;
}
List.Remove(this);
SortContentPackages();
}
}
@@ -621,18 +629,14 @@ namespace Barotrauma
public string Path;
public ContentType Type;
public Workshop.Item WorkShopItem;
public ContentPackage ContentPackage;
public ContentFile(string path, ContentType type, Workshop.Item workShopItem = null)
public ContentFile(string path, ContentType type, ContentPackage contentPackage = null)
{
Path = path;
#if OSX || LINUX
Path = Path.Replace("\\", "/");
#endif
Path = path.CleanUpPath();
Type = type;
WorkShopItem = workShopItem;
ContentPackage = contentPackage;
}
public override string ToString()
@@ -16,6 +16,7 @@ namespace Barotrauma
public readonly string Name;
public Exception Exception;
public volatile bool AbortRequested;
public Thread Thread;
@@ -57,6 +58,11 @@ namespace Barotrauma
return handle;
}
public static CoroutineHandle Invoke(Action action)
{
return StartCoroutine(DoInvokeAfter(action, 0.0f));
}
public static CoroutineHandle InvokeAfter(Action action, float delay)
{
return StartCoroutine(DoInvokeAfter(action, delay));
@@ -69,7 +75,10 @@ namespace Barotrauma
yield return CoroutineStatus.Failure;
}
yield return new WaitForSeconds(delay);
if (delay > 0.0f)
{
yield return new WaitForSeconds(delay);
}
action();
@@ -79,37 +88,59 @@ namespace Barotrauma
public static bool IsCoroutineRunning(string name)
{
return Coroutines.Any(c => c.Name == name);
lock (Coroutines)
{
return Coroutines.Any(c => c.Name == name);
}
}
public static bool IsCoroutineRunning(CoroutineHandle handle)
{
return Coroutines.Contains(handle);
lock (Coroutines)
{
return Coroutines.Contains(handle);
}
}
public static void StopCoroutines(string name)
{
Coroutines.ForEach(c =>
lock (Coroutines)
{
if (c.Name == name)
Coroutines.ForEach(c =>
{
c.Thread?.Abort();
c.Thread?.Join();
}
});
Coroutines.RemoveAll(c => c.Name == name);
if (c.Name == name)
{
c.AbortRequested = true;
if (c.Thread != null)
{
bool joined = false;
while (!joined)
{
#if CLIENT
CrossThread.ProcessTasks();
#endif
joined = c.Thread.Join(TimeSpan.FromMilliseconds(500));
}
}
}
});
Coroutines.RemoveAll(c => c.Name == name);
}
}
public static void StopCoroutines(CoroutineHandle handle)
{
Coroutines.RemoveAll(c => c == handle);
lock (Coroutines)
{
Coroutines.RemoveAll(c => c == handle);
}
}
public static void ExecuteCoroutineThread(CoroutineHandle handle)
{
try
{
while (true)
while (!handle.AbortRequested)
{
if (handle.Coroutine.Current != null)
{
@@ -155,6 +186,7 @@ namespace Barotrauma
#endif
if (handle.Thread == null)
{
if (handle.AbortRequested) { return true; }
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
@@ -210,9 +242,22 @@ namespace Barotrauma
UnscaledDeltaTime = unscaledDeltaTime;
DeltaTime = deltaTime;
foreach (var x in Coroutines.ToList())
if(IsDone(x))
Coroutines.Remove(x);
List<CoroutineHandle> coroutineList;
lock (Coroutines)
{
coroutineList = Coroutines.ToList();
}
foreach (var coroutine in coroutineList)
{
if (IsDone(coroutine))
{
lock (Coroutines)
{
Coroutines.Remove(coroutine);
}
}
}
}
}
@@ -68,10 +68,9 @@ namespace Barotrauma
if (!CheatsEnabled && IsCheat)
{
NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red);
if (Steam.SteamManager.USE_STEAM)
{
NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red);
}
#if USE_STEAM
NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red);
#endif
return;
}
@@ -99,9 +98,6 @@ namespace Barotrauma
private static string currentAutoCompletedCommand;
private static int currentAutoCompletedIndex;
//used for keeping track of the message entered when pressing up/down
private static int selectedIndex;
public static bool CheatsEnabled;
private static readonly List<ColoredText> unsavedMessages = new List<ColoredText>();
@@ -163,9 +159,9 @@ namespace Barotrauma
commands.Add(new Command("items|itemlist", "itemlist: List all the item prefabs available for spawning.", (string[] args) =>
{
NewMessage("***************", Color.Cyan);
foreach (MapEntityPrefab ep in MapEntityPrefab.List)
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
{
if (!(ep is ItemPrefab itemPrefab) || itemPrefab.Name == null) continue;
if (string.IsNullOrEmpty(itemPrefab.Name)) continue;
string text = $"- {itemPrefab.Name}";
if (itemPrefab.Tags.Any())
{
@@ -192,23 +188,16 @@ namespace Barotrauma
UpdaterUtil.SaveFileList("filelist.xml");
}));
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", (string[] args) =>
{
SpawnCharacter(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), out string errorMsg);
if (!string.IsNullOrWhiteSpace(errorMsg))
{
ThrowError(errorMsg);
}
},
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
() =>
{
List<string> characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).ToList();
List<string> characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).Select(f => f.Path).ToList();
for (int i = 0; i < characterFiles.Count; i++)
{
characterFiles[i] = Path.GetFileNameWithoutExtension(characterFiles[i]).ToLowerInvariant();
}
foreach (JobPrefab jobPrefab in JobPrefab.List.Values)
foreach (JobPrefab jobPrefab in JobPrefab.Prefabs)
{
characterFiles.Add(jobPrefab.Name);
}
@@ -220,12 +209,16 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("spawnitem", "spawnitem [itemname] [cursor/inventory/cargo/random/[name]]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".",
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".",
(string[] args) =>
{
try
{
#if CLIENT
SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), Character.Controlled, out string errorMsg);
#elif SERVER
SpawnItem(args, Vector2.Zero, null, out string errorMsg);
#endif
if (!string.IsNullOrWhiteSpace(errorMsg))
{
ThrowError(errorMsg);
@@ -241,9 +234,9 @@ namespace Barotrauma
() =>
{
List<string> itemNames = new List<string>();
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
{
if (prefab is ItemPrefab itemPrefab) itemNames.Add(itemPrefab.Name);
itemNames.Add(itemPrefab.Name);
}
List<string> spawnPosParams = new List<string>() { "cursor", "inventory" };
@@ -289,6 +282,8 @@ namespace Barotrauma
commands.Add(new Command("botspawnmode", "botspawnmode [fill/normal]: Set how bots are spawned in the multiplayer.", null));
commands.Add(new Command("killdisconnectedtimer", "killdisconnectedtimer [seconds]: Set the time after which disconnect players' characters get automatically killed.", null));
commands.Add(new Command("autorestart", "autorestart [true/false]: Enable or disable round auto-restart.", null));
commands.Add(new Command("autorestartinterval", "autorestartinterval [seconds]: Set how long the server waits between rounds before automatically starting a new one. If set to 0, autorestart is disabled.", null));
@@ -297,11 +292,41 @@ namespace Barotrauma
commands.Add(new Command("startwhenclientsready", "startwhenclientsready [true/false]: Enable or disable automatically starting the round when clients are ready to start.", null));
commands.Add(new Command("giveperm", "giveperm [id]: Grants administrative permissions to the player with the specified client ID.", null));
commands.Add(new Command("giveperm", "giveperm [id]: Grants administrative permissions to the player with the specified client ID.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
commands.Add(new Command("revokeperm", "revokeperm [id]: Revokes administrative permissions to the player with the specified client ID.", null));
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
Enum.GetValues(typeof(ClientPermissions)).Cast<ClientPermissions>().Select(v => v.ToString()).ToArray()
};
}));
commands.Add(new Command("revokeperm", "revokeperm [id]: Revokes administrative permissions to the player with the specified client ID.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
Enum.GetValues(typeof(ClientPermissions)).Cast<ClientPermissions>().Select(v => v.ToString()).ToArray()
};
}));
commands.Add(new Command("giverank", "giverank [id]: Assigns a specific rank (= a set of administrative permissions) to the player with the specified client ID.", null));
commands.Add(new Command("giverank", "giverank [id]: Assigns a specific rank (= a set of administrative permissions) to the player with the specified client ID.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
PermissionPreset.List.Select(pp => pp.Name).ToArray()
};
}));
commands.Add(new Command("givecommandperm", "givecommandperm [id]: Gives the player with the specified client ID the permission to use the specified console commands.", null));
@@ -447,17 +472,7 @@ namespace Barotrauma
commands.Add(new Command("banendpoint|banip", "banendpoint [endpoint]: Ban the IP address/SteamID from the server.", null));
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", (string[] args) =>
{
Character tpCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false);
if (tpCharacter == null) return;
var cam = GameMain.GameScreen.Cam;
tpCharacter.AnimController.CurrentHull = null;
tpCharacter.Submarine = null;
tpCharacter.AnimController.SetPosition(ConvertUnits.ToSimUnits(cam.ScreenToWorld(PlayerInput.MousePosition)));
tpCharacter.AnimController.FindHull(cam.ScreenToWorld(PlayerInput.MousePosition), true);
},
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null,
() =>
{
return new string[][]
@@ -498,7 +513,7 @@ namespace Barotrauma
try
{
int count = args.Length == 0 ? 10 : int.Parse(args[0]);
Entity.DumpIds(count);
Entity.DumpIds(count, args.Length >= 2 ? args[1] : null);
}
catch (Exception e)
{
@@ -506,20 +521,27 @@ namespace Barotrauma
}
}));
commands.Add(new Command("dumptofile", "", (string[] args) =>
{
string filename = "consoleOutput.txt";
if (args.Length > 0) { filename = string.Join(" ", args); }
File.WriteAllLines(filename, Messages.Select(m => m.Text).ToArray());
}));
commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) =>
{
if (args.Length == 0) return;
args[0] = args[0].ToLowerInvariant();
if (args.Length == 0) { return; }
foreach (MapEntity mapEntity in MapEntity.mapEntityList)
{
if (mapEntity.Name.ToLowerInvariant() == args[0])
if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase))
{
ThrowError(mapEntity.ID + ": " + mapEntity.Name.ToString());
}
}
foreach (Character character in Character.CharacterList)
{
if (character.Name.ToLowerInvariant() == args[0] || character.SpeciesName.ToLowerInvariant() == args[0])
if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName.Equals(args[0], StringComparison.OrdinalIgnoreCase))
{
ThrowError(character.ID + ": " + character.Name.ToString());
}
@@ -530,9 +552,9 @@ namespace Barotrauma
{
if (args.Length < 2) return;
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.Find(a =>
a.Name.ToLowerInvariant() == args[0].ToLowerInvariant() ||
a.Identifier.ToLowerInvariant() == args[0].ToLowerInvariant());
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a =>
a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) ||
a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
ThrowError("Affliction \"" + args[0] + "\" not found.");
@@ -659,17 +681,7 @@ namespace Barotrauma
NewMessage(Hull.EditFire ? "Fire spawning on" : "Fire spawning off", Color.White);
}, isCheat: true));
commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [emp strength]: Creates an explosion at the position of the cursor.", (string[] args) =>
{
Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
float range = 500, force = 10, damage = 50, structureDamage = 10, empStrength = 0.0f;
if (args.Length > 0) float.TryParse(args[0], out range);
if (args.Length > 1) float.TryParse(args[1], out force);
if (args.Length > 2) float.TryParse(args[2], out damage);
if (args.Length > 3) float.TryParse(args[3], out structureDamage);
if (args.Length > 4) float.TryParse(args[4], out empStrength);
new Explosion(range, force, damage, structureDamage, empStrength).Explode(explosionPos, null);
}, isCheat: true));
commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [emp strength]: Creates an explosion at the position of the cursor.", null, isCheat: true));
commands.Add(new Command("showseed|showlevelseed", "showseed: Show the seed of the current level.", (string[] args) =>
{
@@ -693,7 +705,7 @@ namespace Barotrauma
{
if (Submarine.MainSub == null || Level.Loaded == null) return;
if (args.Length > 0 && args[0].ToLowerInvariant() == "start")
if (args.Length > 0 && args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
}
@@ -703,9 +715,24 @@ namespace Barotrauma
}
}, isCheat: true));
commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) =>
{
if (args.Length == 0) { return; }
Character character = FindMatchingCharacter(args, false);
if (character == null) { return; }
Entity.Spawner?.AddToRemoveQueue(character);
},
() =>
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
};
}, isCheat: true));
commands.Add(new Command("waterphysicsparams", "waterphysicsparams [stiffness] [spread] [damping]: defaults 0.02, 0.05, 0.05", (string[] args) =>
{
Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
float stiffness = 0.02f, spread = 0.05f, damp = 0.01f;
if (args.Length > 0) float.TryParse(args[0], out stiffness);
if (args.Length > 1) float.TryParse(args[1], out spread);
@@ -827,6 +854,7 @@ namespace Barotrauma
var reactor = reactorItem.GetComponent<Reactor>();
reactor.TurbineOutput = power / reactor.MaxPowerOutput * 100.0f;
reactor.FissionRate = power / reactor.MaxPowerOutput * 100.0f;
reactor.PowerOn = true;
reactor.AutoTemp = true;
#if SERVER
@@ -936,6 +964,12 @@ namespace Barotrauma
}));
commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null));
commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) =>
{
AutoItemPlacer.OutputDebugInfo = !AutoItemPlacer.OutputDebugInfo;
NewMessage((AutoItemPlacer.OutputDebugInfo ? "Enabled" : "Disabled") + " automatic item placer logging.", Color.White);
}, isCheat: false));
commands.Add(new Command("verboselogging", "verboselogging: Toggle verbose console logging on/off. When on, additional debug information is written to the debug console.", (string[] args) =>
{
@@ -1083,45 +1117,9 @@ namespace Barotrauma
commands.Sort((c1, c2) => c1.names[0].CompareTo(c2.names[0]));
}
private static string[] SplitCommand(string command)
{
command = command.Trim();
List<string> commands = new List<string>();
int escape = 0;
bool inQuotes = false;
string piece = "";
for (int i = 0; i < command.Length; i++)
{
if (command[i] == '\\')
{
if (escape == 0) escape = 2;
else piece += '\\';
}
else if (command[i] == '"')
{
if (escape == 0) inQuotes = !inQuotes;
else piece += '"';
}
else if (command[i] == ' ' && !inQuotes)
{
if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
piece = "";
}
else if (escape == 0) piece += command[i];
if (escape > 0) escape--;
}
if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece); //add final piece
return commands.ToArray();
}
public static string AutoComplete(string command, int increment = 1)
{
string[] splitCommand = SplitCommand(command);
string[] splitCommand = ToolBox.SplitCommand(command);
string[] args = splitCommand.Skip(1).ToArray();
//if an argument is given or the last character is a space, attempt to autocomplete the argument
@@ -1193,24 +1191,6 @@ namespace Barotrauma
currentAutoCompletedIndex = 0;
}
public static string SelectMessage(int direction, string currentText = null)
{
if (Messages.Count == 0) return "";
direction = MathHelper.Clamp(direction, -1, 1);
int i = 0;
do
{
selectedIndex += direction;
if (selectedIndex < 0) selectedIndex = Messages.Count - 1;
selectedIndex %= Messages.Count;
if (++i >= Messages.Count) break;
} while (!Messages[selectedIndex].IsCommand || Messages[selectedIndex].Text == currentText);
return !Messages[selectedIndex].IsCommand ? "" : Messages[selectedIndex].Text;
}
public static void ExecuteCommand(string command)
{
if (activeQuestionCallback != null)
@@ -1226,9 +1206,9 @@ namespace Barotrauma
return;
}
if (string.IsNullOrWhiteSpace(command) || command == "\\" || command == "\n") return;
if (string.IsNullOrWhiteSpace(command) || command == "\\" || command == "\n") { return; }
string[] splitCommand = SplitCommand(command);
string[] splitCommand = ToolBox.SplitCommand(command);
if (splitCommand.Length == 0)
{
ThrowError("Failed to execute command \"" + command + "\"!");
@@ -1239,29 +1219,35 @@ namespace Barotrauma
return;
}
if (!splitCommand[0].ToLowerInvariant().Equals("admin"))
string firstCommand = splitCommand[0].ToLowerInvariant();
if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase))
{
NewMessage(command, Color.White, true);
}
#if CLIENT
if (GameMain.Client != null)
{
if (GameMain.Client.HasConsoleCommandPermission(splitCommand[0].ToLowerInvariant()))
Command matchingCommand = commands.Find(c => c.names.Contains(firstCommand));
if (matchingCommand == null)
{
Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant()));
//if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
if (matchingCommand == null || matchingCommand.RelayToServer)
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.Cyan);
return;
}
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
{
if (matchingCommand.RelayToServer)
{
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.White);
NewMessage("Server command: " + command, Color.Cyan);
}
else
{
matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
}
return;
}
}
@@ -1270,7 +1256,7 @@ namespace Barotrauma
bool commandFound = false;
foreach (Command c in commands)
{
if (!c.names.Contains(splitCommand[0].ToLowerInvariant())) continue;
if (!c.names.Contains(firstCommand)) { continue; }
c.Execute(splitCommand.Skip(1).ToArray());
commandFound = true;
break;
@@ -1298,7 +1284,7 @@ namespace Barotrauma
}
var matchingCharacters = Character.CharacterList.FindAll(c =>
c.Name.ToLowerInvariant() == characterName &&
c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) &&
(!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c));
if (!matchingCharacters.Any())
@@ -1341,11 +1327,16 @@ namespace Barotrauma
WayPoint spawnPoint = null;
string characterLowerCase = args[0].ToLowerInvariant();
if (!JobPrefab.List.TryGetValue(characterLowerCase, out JobPrefab job))
JobPrefab job = null;
if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase))
{
job = JobPrefab.List.Values.FirstOrDefault(jp => jp.Name?.ToLowerInvariant() == characterLowerCase);
job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase));
}
bool human = job != null || characterLowerCase == Character.HumanSpeciesName;
else
{
job = JobPrefab.Prefabs[characterLowerCase];
}
bool human = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
if (args.Length > 1)
{
@@ -1395,13 +1386,9 @@ namespace Barotrauma
if (human)
{
CharacterInfo characterInfo = new CharacterInfo(Character.HumanSpeciesName, jobPrefab: job);
var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.Server) : 0;
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant);
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
if (job != null)
{
spawnedCharacter.GiveJobItems(spawnPoint);
}
if (GameMain.GameSession != null)
{
if (GameMain.GameSession.GameMode != null && !GameMain.GameSession.GameMode.IsSinglePlayer)
@@ -1413,10 +1400,12 @@ namespace Barotrauma
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
}
spawnedCharacter.GiveJobItems(spawnPoint);
spawnedCharacter.Info.StartItemsGiven = true;
}
else
{
if (Character.GetConfigFilePath(args[0]) != null)
if (CharacterPrefab.FindBySpeciesName(args[0]) != null)
{
Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8));
}
@@ -1431,11 +1420,14 @@ namespace Barotrauma
Vector2? spawnPos = null;
Inventory spawnInventory = null;
string itemName = args[0].ToLowerInvariant();
if (!(MapEntityPrefab.Find(itemName, showErrorMessages: false) is ItemPrefab itemPrefab))
string itemNameOrId = args[0].ToLowerInvariant();
ItemPrefab itemPrefab =
(MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab;
if (itemPrefab == null)
{
errorMsg = "Item \"" + itemName + "\" not found!";
var matching = MapEntityPrefab.List.Find(me => me.Name.ToLowerInvariant().StartsWith(itemName) && me is ItemPrefab);
errorMsg = "Item \"" + itemNameOrId + "\" not found!";
var matching = ItemPrefab.Prefabs.Find(me => me.Name.ToLowerInvariant().StartsWith(itemNameOrId) && me is ItemPrefab);
if (matching != null)
{
errorMsg += $" Did you mean \"{matching.Name}\"?";
@@ -1504,6 +1496,9 @@ namespace Barotrauma
public static void NewMessage(string msg, bool isCommand = false)
{
#if DEBUG
Console.WriteLine(msg);
#endif
NewMessage(msg, Color.White, isCommand);
}
@@ -1517,8 +1512,14 @@ namespace Barotrauma
}
}
public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered)
public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1)
{
if (args != null && args.Length > argCount)
{
onAnswered(args[argCount]);
return;
}
#if CLIENT
activeQuestionText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, 0), listBox.Content.RectTransform),
" >>" + question, font: GUI.SmallFont, wrap: true)
@@ -1587,12 +1588,7 @@ namespace Barotrauma
return true;
}
public static Command FindCommand(string commandName)
{
commandName = commandName.ToLowerInvariant();
return commands.Find(c => c.names.Any(n => n.ToLowerInvariant() == commandName));
}
public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase)));
public static void Log(string message)
{
@@ -1610,8 +1606,24 @@ namespace Barotrauma
}
}
System.Diagnostics.Debug.WriteLine(error);
NewMessage(error, Color.Red);
#if CLIENT
var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform),
style: "InnerFrame", color: Color.White)
{
CanBeFocused = false
};
var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) },
error, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true)
{
CanBeFocused = false,
TextColor = Color.Red
};
textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5);
textBlock.SetTextPos();
listBox.UpdateScrollBarSize();
listBox.BarScroll = 1.0f;
if (createMessageBox)
{
CoroutineManager.StartCoroutine(CreateMessageBox(error));
@@ -1620,6 +1632,8 @@ namespace Barotrauma
{
isOpen = true;
}
#else
NewMessage(error, Color.Red);
#endif
}
@@ -62,7 +62,7 @@ namespace Barotrauma
private void SpawnItem()
{
item = new Item(itemPrefab, spawnPos, null);
item.body.FarseerBody.IsKinematic = true;
item.body.FarseerBody.BodyType = FarseerPhysics.BodyType.Kinematic;
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
@@ -98,7 +98,7 @@ namespace Barotrauma
switch (state)
{
case 0:
if (item.ParentInventory != null) item.body.FarseerBody.IsKinematic = false;
if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic; }
if (item.CurrentHull == null) return;
state = 1;
@@ -0,0 +1,517 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class EventManager
{
const float IntensityUpdateInterval = 5.0f;
private Level level;
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
//The "intensity" of the current situation (a value between 0.0 - 1.0).
//High when a disaster has struck, low when nothing special is going on.
private float currentIntensity;
//The exact intensity of the current situation, current intensity is lerped towards this value
private float targetIntensity;
//How low the intensity has to be for an event to be triggered.
//Gradually increases with time, so additional problems can still appear eventually even if
//the sub is laying broken on the ocean floor or if the players are trying to abuse the system
//by intentionally keeping the intensity high by causing breaches, damaging themselves or such
private float eventThreshold = 0.2f;
//New events can't be triggered when the cooldown is active.
private float eventCoolDown;
private float intensityUpdateTimer;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger;
private float roundDuration;
private readonly List<ScriptedEventSet> pendingEventSets = new List<ScriptedEventSet>();
private readonly Dictionary<ScriptedEventSet, List<ScriptedEvent>> selectedEvents = new Dictionary<ScriptedEventSet, List<ScriptedEvent>>();
private readonly List<ScriptedEvent> activeEvents = new List<ScriptedEvent>();
#if DEBUG && SERVER
private DateTime nextIntensityLogTime;
#endif
private EventManagerSettings settings;
private readonly bool isClient;
public float CurrentIntensity
{
get { return currentIntensity; }
}
public List<ScriptedEvent> ActiveEvents
{
get { return activeEvents; }
}
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
}
public bool Enabled = true;
public void StartRound(Level level)
{
if (isClient) { return; }
pendingEventSets.Clear();
selectedEvents.Clear();
this.level = level;
SelectSettings();
var initialEventSet = SelectRandomEvents(ScriptedEventSet.List);
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet);
}
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
intensityUpdateTimer = 0.0f;
CalculateCurrentIntensity(0.0f);
currentIntensity = targetIntensity;
eventCoolDown = 0.0f;
}
private void SelectSettings()
{
if (EventManagerSettings.List.Count == 0)
{
throw new InvalidOperationException("Could not select EventManager settings (no settings loaded).");
}
if (level == null)
{
throw new InvalidOperationException("Could not select EventManager settings (level not set).");
}
var suitableSettings = EventManagerSettings.List.FindAll(s =>
level.Difficulty >= s.MinLevelDifficulty &&
level.Difficulty <= s.MaxLevelDifficulty);
if (suitableSettings.Count == 0)
{
DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")");
settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)];
}
else
{
settings = suitableSettings[Rand.Int(suitableSettings.Count, Rand.RandSync.Server)];
}
if (settings != null)
{
eventThreshold = settings.DefaultEventThreshold;
}
}
public IEnumerable<ContentFile> GetFilesToPreload()
{
foreach (List<ScriptedEvent> eventList in selectedEvents.Values)
{
foreach (ScriptedEvent scriptedEvent in eventList)
{
foreach (ContentFile contentFile in scriptedEvent.GetFilesToPreload())
{
yield return contentFile;
}
}
}
}
public void PreloadContent(IEnumerable<ContentFile> contentFiles)
{
foreach (ContentFile file in contentFiles)
{
switch (file.Type)
{
case ContentType.Character:
#if CLIENT
CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(file.Path);
if (characterPrefab?.XDocument == null)
{
throw new Exception($"Failed to load the character config file from {file.Path}!");
}
var doc = characterPrefab.XDocument;
var rootElement = doc.Root;
var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
foreach (var soundElement in mainElement.GetChildElements("sound"))
{
var sound = Submarine.LoadRoundSound(soundElement);
}
string speciesName = mainElement.GetAttributeString("speciesname", null);
if (string.IsNullOrWhiteSpace(speciesName))
{
speciesName = mainElement.GetAttributeString("name", null);
if (!string.IsNullOrWhiteSpace(speciesName))
{
DebugConsole.NewMessage($"Error in {file.Path}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
throw new Exception($"Species name null in {file.Path}");
}
}
bool humanoid = mainElement.GetAttributeBool("humanoid", false);
RagdollParams ragdollParams;
if (humanoid)
{
ragdollParams = RagdollParams.GetRagdollParams<HumanRagdollParams>(speciesName);
}
else
{
ragdollParams = RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName);
}
if (ragdollParams != null)
{
HashSet<string> texturePaths = new HashSet<string>
{
ragdollParams.Texture
};
foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs)
{
if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); }
if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); }
if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); }
foreach (var decorativeSprite in limb.decorativeSpriteParams)
{
if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); }
}
}
foreach (string texturePath in texturePaths)
{
preloadedSprites.Add(new Sprite(texturePath, Vector2.Zero));
}
}
#endif
break;
}
}
}
public void EndRound()
{
pendingEventSets.Clear();
selectedEvents.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
}
private void CreateEvents(ScriptedEventSet eventSet)
{
int applyCount = 1;
if (eventSet.PerRuin)
{
applyCount = Level.Loaded.Ruins.Count();
}
for (int i = 0; i < applyCount; i++)
{
if (eventSet.ChooseRandom)
{
if (eventSet.EventPrefabs.Count > 0)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand);
if (eventPrefab != null)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
}
selectedEvents[eventSet].Add(newEvent);
}
}
if (eventSet.ChildSets.Count > 0)
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets);
if (newEventSet != null) { CreateEvents(newEventSet); }
}
}
else
{
foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
}
selectedEvents[eventSet].Add(newEvent);
}
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
{
CreateEvents(childEventSet);
}
}
}
}
private ScriptedEventSet SelectRandomEvents(List<ScriptedEventSet> eventSets)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var allowedEventSets =
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty);
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
float randomNumber = (float)rand.NextDouble() * totalCommonness;
foreach (ScriptedEventSet eventSet in allowedEventSets)
{
float commonness = eventSet.GetCommonness(level);
if (randomNumber <= commonness)
{
return eventSet;
}
randomNumber -= commonness;
}
return null;
}
private bool CanStartEventSet(ScriptedEventSet eventSet)
{
float distFromStart = Vector2.Distance(Submarine.MainSub.WorldPosition, level.StartPosition);
float distFromEnd = Vector2.Distance(Submarine.MainSub.WorldPosition, level.EndPosition);
float distanceTraveled = MathHelper.Clamp(
(Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X),
0.0f, 1.0f);
//don't create new events if within 50 meters of the start/end of the level
if (!eventSet.AllowAtStart)
{
if (distanceTraveled <= 0.0f ||
distFromStart * Physics.DisplayToRealWorldRatio < 50.0f ||
distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f)
{
return false;
}
}
if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) &&
roundDuration < eventSet.MinMissionTime)
{
return false;
}
if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity)
{
return false;
}
return true;
}
public void Update(float deltaTime)
{
if (!Enabled) { return; }
//clients only calculate the intensity but don't create any events
//(the intensity is used for controlling the background music)
CalculateCurrentIntensity(deltaTime);
#if DEBUG && SERVER
if (DateTime.Now > nextIntensityLogTime)
{
DebugConsole.NewMessage("EventManager intensity: " + (int)Math.Round(currentIntensity * 100) + " %");
nextIntensityLogTime = DateTime.Now + new TimeSpan(0, minutes: 1, seconds: 0);
}
#endif
if (isClient) { return; }
roundDuration += deltaTime;
if (settings == null)
{
DebugConsole.ThrowError("Event settings not set before updating EventManager. Attempting to select...");
SelectSettings();
if (settings == null)
{
DebugConsole.ThrowError("Could not select EventManager settings. Disabling EventManager for the round...");
#if SERVER
GameMain.Server?.SendChatMessage("Could not select EventManager settings. Disabling EventManager for the round...", Networking.ChatMessageType.Error);
#endif
Enabled = false;
return;
}
}
eventThreshold += settings.EventThresholdIncrease * deltaTime;
if (eventCoolDown > 0.0f)
{
eventCoolDown -= deltaTime;
}
else if (currentIntensity < eventThreshold)
{
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
{
var eventSet = pendingEventSets[i];
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (!selectedEvents.ContainsKey(eventSet))
{
//no events selected from this event set
continue;
}
//start events in this set
foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet])
{
activeEvents.Add(scriptedEvent);
}
//add child event sets to pending
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
{
if (selectedEvents.ContainsKey(childEventSet))
{
pendingEventSets.Add(childEventSet);
}
}
}
eventThreshold = settings.DefaultEventThreshold;
eventCoolDown = settings.EventCooldown;
}
foreach (ScriptedEvent ev in activeEvents)
{
if (!ev.IsFinished) { ev.Update(deltaTime); }
}
}
private void CalculateCurrentIntensity(float deltaTime)
{
intensityUpdateTimer -= deltaTime;
if (intensityUpdateTimer > 0.0f) { return; }
intensityUpdateTimer = IntensityUpdateInterval;
// crew health --------------------------------------------------------
avgCrewHealth = 0.0f;
int characterCount = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.TeamID == Character.TeamType.FriendlyNPC) { continue; }
if (character.AIController is HumanAIController || character.IsRemotePlayer)
{
avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f);
characterCount++;
}
}
if (characterCount > 0)
{
avgCrewHealth /= characterCount;
}
else
{
avgCrewHealth = 0.5f;
}
// enemy amount --------------------------------------------------------
enemyDanger = 0.0f;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.IsUnconscious || !character.Enabled) continue;
EnemyAIController enemyAI = character.AIController as EnemyAIController;
if (enemyAI == null) continue;
if (character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
//crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f
enemyDanger += enemyAI.CombatStrength / 1000.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
//enemy outside and targeting the sub or something in it
//moloch adds 0.24 to enemy danger, a crawler 0.02
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
// hull status (gaps, flooding, fire) --------------------------------------------------------
float holeCount = 0.0f;
floodingAmount = 0.0f;
int hullCount = 0;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; }
hullCount++;
foreach (Gap gap in hull.ConnectedGaps)
{
if (!gap.IsRoomToRoom) holeCount += gap.Open;
}
floodingAmount += hull.WaterVolume / hull.Volume;
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
}
if (hullCount > 0)
{
floodingAmount = floodingAmount / hullCount;
}
//hull integrity at 0.0 if there are 10 or more wide-open holes
avgHullIntegrity = MathHelper.Clamp(1.0f - holeCount / 10.0f, 0.0f, 1.0f);
//a fire of any size bumps up the fire amount to 20%
//if the total width of the fires is 1000 or more, the fire amount is considered to be at 100%
fireAmount = MathHelper.Clamp(fireAmount / 1000.0f, fireAmount > 0.0f ? 0.2f : 0.0f, 1.0f);
//flooding less than 10% of the sub is ignored
//to prevent ballast tanks from affecting the intensity
if (floodingAmount < 0.1f) floodingAmount = 0.0f;
// calculate final intensity --------------------------------------------------------
targetIntensity =
((1.0f - avgCrewHealth) + (1.0f - avgHullIntegrity) + floodingAmount) / 3.0f;
targetIntensity += fireAmount * 0.5f;
targetIntensity += enemyDanger;
targetIntensity = MathHelper.Clamp(targetIntensity, 0.0f, 1.0f);
if (targetIntensity > currentIntensity)
{
//50 seconds for intensity to go from 0.0 to 1.0
currentIntensity = MathHelper.Min(currentIntensity + 0.02f * IntensityUpdateInterval, targetIntensity);
}
else
{
//400 seconds for intensity to go from 1.0 to 0.0
currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity);
}
}
}
}
@@ -21,23 +21,20 @@ namespace Barotrauma
public readonly float EventCooldown = 360.0f;
public readonly float MinEventDifficulty = 0.0f;
public readonly float MaxEventDifficulty = 100.0f;
public readonly float MinLevelDifficulty = 0.0f;
public readonly float MaxLevelDifficulty = 100.0f;
static EventManagerSettings()
{
foreach (string file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings))
foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings))
{
Load(file);
}
}
private static void Load(string file)
private static void Load(ContentFile file)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var mainElement = doc.Root;
bool allowOverriding = false;
@@ -55,12 +52,12 @@ namespace Barotrauma
{
if (allowOverriding || subElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file}'", Color.Yellow);
DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file.Path}'", Color.Yellow);
List.Remove(duplicate);
}
else
{
DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{identifier}' found! Each element must have a unique name. Use <override></override> tags if you want to override an existing preset.");
DebugConsole.ThrowError($"Error in '{file.Path}': Another element with the name '{identifier}' found! Each element must have a unique name. Use <override></override> tags if you want to override an existing preset.");
continue;
}
}
@@ -77,9 +74,6 @@ namespace Barotrauma
DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f);
EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f);
MinEventDifficulty = element.GetAttributeFloat("MinEventDifficulty", 0.0f);
MaxEventDifficulty = element.GetAttributeFloat("MaxEventDifficulty", 100.0f);
MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", 0.0f);
MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", 100.0f);
}
@@ -5,11 +5,11 @@ using System.Xml.Linq;
namespace Barotrauma
{
class CargoMission : Mission
partial class CargoMission : Mission
{
private XElement itemConfig;
private readonly XElement itemConfig;
private List<Item> items;
private readonly List<Item> items = new List<Item>();
private int requiredDeliveryAmount;
@@ -22,7 +22,7 @@ namespace Barotrauma
private void InitItems()
{
items = new List<Item>();
items.Clear();
if (itemConfig == null)
{
@@ -35,7 +35,7 @@ namespace Barotrauma
LoadItemAsChild(subElement, null);
}
if (requiredDeliveryAmount == 0) requiredDeliveryAmount = items.Count;
if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; }
}
private void LoadItemAsChild(XElement element, Item parent)
@@ -90,8 +90,6 @@ namespace Barotrauma
var item = new Item(itemPrefab, position, cargoRoom.Submarine);
item.FindHull();
items.Add(item);
if (parent != null) parent.Combine(item, user: null);
@@ -108,7 +106,10 @@ namespace Barotrauma
public override void Start(Level level)
{
InitItems();
if (!IsClient)
{
InitItems();
}
}
public override void End()
@@ -127,8 +128,9 @@ namespace Barotrauma
foreach (Item item in items)
{
if (!item.Removed) item.Remove();
if (!item.Removed) { item.Remove(); }
}
items.Clear();
}
}
}
@@ -8,7 +8,7 @@ namespace Barotrauma
private Submarine[] subs;
private List<Character>[] crews;
private string[] descriptions;
private readonly string[] descriptions;
private static string[] teamNames = { "Team A", "Team B" };
public override bool AllowRespawn
@@ -44,6 +44,11 @@ namespace Barotrauma
}
}
public override int TeamCount
{
get { return 2; }
}
public CombatMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
@@ -113,7 +118,7 @@ namespace Barotrauma
{
for (int i = 0; i < 2; i++)
{
if (wifiComponent.Item.Submarine == subs[i] || subs[i].DockedTo.Contains(wifiComponent.Item.Submarine))
if (wifiComponent.Item.Submarine == subs[i] || subs[i].ConnectedDockingPorts.ContainsKey(wifiComponent.Item.Submarine))
{
wifiComponent.TeamID = subs[i].TeamID;
}
@@ -1,11 +1,12 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
partial class Mission
abstract partial class Mission
{
public readonly MissionPrefab Prefab;
protected bool completed;
@@ -73,6 +74,11 @@ namespace Barotrauma
get { return true; }
}
public virtual int TeamCount
{
get { return 1; }
}
public virtual IEnumerable<Vector2> SonarPositions
{
get { return Enumerable.Empty<Vector2>(); }
@@ -82,6 +88,10 @@ namespace Barotrauma
{
get { return Prefab.SonarLabel; }
}
public string SonarIconIdentifier
{
get { return Prefab.SonarIconIdentifier; }
}
public readonly Location[] Locations;
@@ -131,7 +141,7 @@ namespace Barotrauma
}
else
{
allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.type)) != 0));
allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.Type)) != 0));
}
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
@@ -163,18 +173,17 @@ namespace Barotrauma
public virtual void Update(float deltaTime) { }
public virtual bool AssignTeamIDs(List<Networking.Client> clients)
public virtual void AssignTeamIDs(List<Networking.Client> clients)
{
clients.ForEach(c => c.TeamID = Character.TeamType.Team1);
return false;
}
protected void ShowMessage(int index)
protected void ShowMessage(int missionState)
{
ShowMessageProjSpecific(index);
ShowMessageProjSpecific(missionState);
}
partial void ShowMessageProjSpecific(int index);
partial void ShowMessageProjSpecific(int missionState);
/// <summary>
/// End the mission and give a reward if it was completed successfully
@@ -188,9 +197,7 @@ namespace Barotrauma
public void GiveReward()
{
var mode = GameMain.GameSession.GameMode as CampaignMode;
if (mode == null) return;
if (!(GameMain.GameSession.GameMode is CampaignMode mode)) { return; }
mode.Money += Reward;
}
}
@@ -31,7 +31,7 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public readonly MissionType type;
public readonly MissionType Type;
public readonly bool MultiplayerOnly, SingleplayerOnly;
@@ -44,6 +44,7 @@ namespace Barotrauma
public readonly string SuccessMessage;
public readonly string FailureMessage;
public readonly string SonarLabel;
public readonly string SonarIconIdentifier;
public readonly string AchievementIdentifier;
@@ -61,10 +62,11 @@ namespace Barotrauma
public static void Init()
{
List.Clear();
var files = GameMain.Instance.GetFilesOfType(ContentType.Missions);
foreach (string file in files)
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
bool allowOverride = false;
var mainElement = doc.Root;
@@ -83,12 +85,12 @@ namespace Barotrauma
{
if (allowOverride || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding a mission with the identifier '{identifier}' using the file '{file}'", Color.Yellow);
DebugConsole.NewMessage($"Overriding a mission with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow);
List.Remove(duplicate);
}
else
{
DebugConsole.ThrowError($"Duplicate mission found with the identifier '{identifier}' in file '{file}'! Add <override></override> tags as the parent of the mission definition to allow overriding.");
DebugConsole.ThrowError($"Duplicate mission found with the identifier '{identifier}' in file '{file.Path}'! Add <override></override> tags as the parent of the mission definition to allow overriding.");
// TODO: Don't allow adding duplicates when the issue with multiple missions is solved.
//continue;
}
@@ -122,7 +124,8 @@ namespace Barotrauma
FailureMessage = element.GetAttributeString("failuremessage", "");
}
SonarLabel = TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? element.GetAttributeString("sonarlabel", "");
SonarLabel = TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? element.GetAttributeString("sonarlabel", "");
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false);
@@ -151,18 +154,18 @@ namespace Barotrauma
}
string missionTypeName = element.GetAttributeString("type", "");
if (!Enum.TryParse(missionTypeName, out type))
if (!Enum.TryParse(missionTypeName, out Type))
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
return;
}
if (type == MissionType.None)
if (Type == MissionType.None)
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
return;
}
constructor = missionClasses[type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
constructor = missionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
InitProjSpecific(element);
}
@@ -173,11 +176,11 @@ namespace Barotrauma
{
foreach (Pair<string, string> allowedLocationType in AllowedLocationTypes)
{
if (allowedLocationType.First.ToLowerInvariant() == "any" ||
allowedLocationType.First.ToLowerInvariant() == from.Type.Identifier.ToLowerInvariant())
if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
if (allowedLocationType.Second.ToLowerInvariant() == "any" ||
allowedLocationType.Second.ToLowerInvariant() == to.Type.Identifier.ToLowerInvariant())
if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -186,7 +189,7 @@ namespace Barotrauma
return false;
}
public Mission Instantiate(Location[] locations)
{
return constructor?.Invoke(new object[] { this, locations }) as Mission;
@@ -0,0 +1,197 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
partial class MonsterMission : Mission
{
private readonly string monsterFile;
private readonly int monsterCount;
//string = filename, point = min,max
private readonly HashSet<Tuple<string, Point>> monsterFiles = new HashSet<Tuple<string, Point>>();
private readonly List<Character> monsters = new List<Character>();
private readonly List<Vector2> sonarPositions = new List<Vector2>();
private readonly List<Vector2> tempSonarPositions = new List<Vector2>();
private readonly float maxSonarMarkerDistance = 10000.0f;
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else
{
return sonarPositions;
}
}
}
public MonsterMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", null);
if (!string.IsNullOrEmpty(monsterFile))
{
var characterPrefab = CharacterPrefab.FindByFilePath(monsterFile);
if (characterPrefab != null)
{
monsterFile = characterPrefab.Identifier;
}
}
maxSonarMarkerDistance = prefab.ConfigElement.GetAttributeFloat("maxsonarmarkerdistance", 10000.0f);
monsterCount = Math.Min(prefab.ConfigElement.GetAttributeInt("monstercount", 1), 255);
string monsterFileName = monsterFile;
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
string monster = monsterElement.GetAttributeString("character", string.Empty);
if (monsterFileName == null)
{
monsterFileName = monster;
}
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = monsterElement.GetAttributeInt("amount", 1);
}
int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255);
monsterFiles.Add(new Tuple<string, Point>(monster, new Point(min, max)));
}
description = description.Replace("[monster]",
TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFileName)));
}
public override void Start(Level level)
{
if (monsters.Count > 0)
{
throw new Exception($"monsters.Count > 0 ({monsters.Count})");
}
if (tempSonarPositions.Count > 0)
{
throw new Exception($"tempSonarPositions.Count > 0 ({tempSonarPositions.Count})");
}
if (!IsClient)
{
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
if (!string.IsNullOrEmpty(monsterFile))
{
for (int i = 0; i < monsterCount; i++)
{
monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
foreach (var monster in monsterFiles)
{
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
for (int i = 0; i < amount; i++)
{
monsters.Add(Character.Create(monster.Item1, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
InitializeMonsters(monsters);
}
}
private void InitializeMonsters(IEnumerable<Character> monsters)
{
monsters.ForEach(m => m.Enabled = false);
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
foreach (Character monster in monsters)
{
tempSonarPositions.Add(monster.WorldPosition + Rand.Vector(maxSonarMarkerDistance));
}
if (monsters.Count() != tempSonarPositions.Count)
{
throw new Exception($"monsters.Count != tempSonarPositions.Count ({monsters.Count()} != {tempSonarPositions.Count})");
}
}
public override void Update(float deltaTime)
{
switch (State)
{
case 0:
//keep sonar markers within maxSonarMarkerDistance from the monster(s)
for (int i = 0; i < tempSonarPositions.Count; i++)
{
if (monsters.Count != tempSonarPositions.Count)
{
throw new Exception($"monsters.Count != tempSonarPositions.Count ({monsters.Count} != {tempSonarPositions.Count})");
}
if (i < 0 || i >= monsters.Count)
{
throw new Exception($"Index {i} outside of bounds 0-{monsters.Count} ({tempSonarPositions.Count})");
}
if (monsters[i].Removed || monsters[i].IsDead) { continue; }
Vector2 diff = tempSonarPositions[i] - monsters[i].Position;
float maxDist = maxSonarMarkerDistance;
Submarine refSub = Character.Controlled?.Submarine ?? Submarine.MainSub;
if (refSub != null)
{
Vector2 refPos = refSub == null ? Vector2.Zero : refSub.WorldPosition;
float subDist = Vector2.Distance(refPos, tempSonarPositions[i]) / maxDist;
maxDist = Math.Min(subDist * subDist * maxDist, maxDist);
maxDist = Math.Min(Vector2.Distance(refPos, monsters[i].Position), maxDist);
}
if (diff.LengthSquared() > maxDist * maxDist)
{
tempSonarPositions[i] = monsters[i].Position + Vector2.Normalize(diff) * maxDist;
}
}
sonarPositions.Clear();
for (int i = 0; i < monsters.Count; i++)
{
if (monsters[i].Removed || monsters[i].IsDead) { continue; }
//don't add another label if there's another monster roughly at the same spot
if (sonarPositions.All(p => Vector2.DistanceSquared(p, tempSonarPositions[i]) > 1000.0f * 1000.0f))
{
sonarPositions.Add(tempSonarPositions[i]);
}
}
if (!IsClient && monsters.All(m => IsEliminated(m)))
{
State = 1;
}
break;
}
}
public override void End()
{
tempSonarPositions.Clear();
monsters.Clear();
if (State < 1) { return; }
GiveReward();
completed = true;
}
public bool IsEliminated(Character enemy) => enemy.Removed || enemy.IsDead || enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee;
}
}
@@ -6,19 +6,19 @@ using System.Linq;
namespace Barotrauma
{
class SalvageMission : Mission
partial class SalvageMission : Mission
{
private ItemPrefab itemPrefab;
private readonly ItemPrefab itemPrefab;
private Item item;
private Level.PositionType spawnPositionType;
private readonly Level.PositionType spawnPositionType;
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0 )
if (item == null)
{
Enumerable.Empty<Vector2>();
}
@@ -62,23 +62,26 @@ namespace Barotrauma
public override void Start(Level level)
{
//ruin items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin ? 0.0f : Level.Loaded.Size.X * 0.3f;
Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
item = new Item(itemPrefab, position, null);
item.body.FarseerBody.IsKinematic = true;
if (item.HasTag("alien"))
if (!IsClient)
{
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
{
if (it.Submarine != null || !it.HasTag("artifactholder")) continue;
//ruin items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin ? 0.0f : Level.Loaded.Size.X * 0.3f;
Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
item = new Item(itemPrefab, position, null);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) continue;
if (itemContainer.Combine(item, user: null)) break; // Placement successful
if (item.HasTag("alien"))
{
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
{
if (it.Submarine != null || !it.HasTag("artifactholder")) continue;
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) { continue; }
if (itemContainer.Combine(item, user: null)) { break; } // Placement successful
}
}
}
}
@@ -87,13 +90,13 @@ namespace Barotrauma
{
if (IsClient)
{
if (item.ParentInventory != null) { item.body.FarseerBody.IsKinematic = false; }
if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
return;
}
switch (State)
{
case 0:
if (item.ParentInventory != null) { item.body.FarseerBody.IsKinematic = false; }
if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
if (item.CurrentHull?.Submarine == null) { return; }
State = 1;
break;
@@ -108,7 +111,8 @@ namespace Barotrauma
{
if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) { return; }
item.Remove();
item?.Remove();
item = null;
GiveReward();
completed = true;
}
@@ -9,49 +9,55 @@ namespace Barotrauma
{
class MonsterEvent : ScriptedEvent
{
private string characterFile;
private int minAmount, maxAmount;
private readonly string speciesName;
private readonly int minAmount, maxAmount;
private List<Character> monsters;
private bool spawnDeep;
private readonly bool spawnDeep;
private Vector2 spawnPos;
private Vector2? spawnPos;
private bool disallowed;
private Level.PositionType spawnPosType;
private readonly bool disallowed;
private readonly Level.PositionType spawnPosType;
private bool spawnPending;
private string characterFileName;
public override Vector2 DebugDrawPos
{
get { return spawnPos; }
get { return spawnPos ?? Vector2.Zero; }
}
public override string ToString()
{
if (maxAmount <= 1)
{
return "MonsterEvent (" + characterFileName + ")";
return "MonsterEvent (" + speciesName + ")";
}
else if (minAmount < maxAmount)
{
return "MonsterEvent (" + characterFileName + " x" + minAmount + "-" + maxAmount + ")";
return "MonsterEvent (" + speciesName + " x" + minAmount + "-" + maxAmount + ")";
}
else
{
return "MonsterEvent (" + characterFileName + " x" + maxAmount + ")";
return "MonsterEvent (" + speciesName + " x" + maxAmount + ")";
}
}
public MonsterEvent(ScriptedEventPrefab prefab)
: base (prefab)
{
characterFile = prefab.ConfigElement.GetAttributeString("characterfile", "");
speciesName = prefab.ConfigElement.GetAttributeString("characterfile", "");
CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesName);
if (characterPrefab != null)
{
speciesName = characterPrefab.Identifier;
}
if (string.IsNullOrEmpty(speciesName))
{
throw new Exception("speciesname is null!");
}
int defaultAmount = prefab.ConfigElement.GetAttributeInt("amount", 1);
minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount);
@@ -66,12 +72,11 @@ namespace Barotrauma
}
spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false);
characterFileName = Path.GetFileName(Path.GetDirectoryName(characterFile)).ToLower();
if (GameMain.NetworkMember != null)
{
List<string> monsterNames = GameMain.NetworkMember.ServerSettings.MonsterEnabled.Keys.ToList();
string tryKey = monsterNames.Find(s => characterFileName == s.ToLower());
string tryKey = monsterNames.Find(s => speciesName.ToLower() == s.ToLower());
if (!string.IsNullOrWhiteSpace(tryKey))
{
@@ -80,6 +85,20 @@ namespace Barotrauma
}
}
public override IEnumerable<ContentFile> GetFilesToPreload()
{
string path = CharacterPrefab.FindBySpeciesName(speciesName)?.FilePath;
if (string.IsNullOrWhiteSpace(path))
{
DebugConsole.ThrowError($"Failed to find config file for species \"{speciesName}\"");
yield break;
}
else
{
yield return new ContentFile(path, ContentType.Character);
}
}
public override bool CanAffectSubImmediately(Level level)
{
float maxRange = Items.Components.Sonar.DefaultSonarRange * 0.8f;
@@ -95,13 +114,12 @@ namespace Barotrauma
return false;
}
public override void Init(bool affectSubImmediately)
{
FindSpawnPosition(affectSubImmediately);
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage("Initialized MonsterEvent (" + characterFile + ")", Color.White);
DebugConsole.NewMessage("Initialized MonsterEvent (" + speciesName + ")", Color.White);
}
}
@@ -131,7 +149,7 @@ namespace Barotrauma
private void FindSpawnPosition(bool affectSubImmediately)
{
if (disallowed) return;
if (disallowed) { return; }
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
@@ -205,10 +223,14 @@ namespace Barotrauma
Finished();
return;
}
if (isFinished) return;
//isActive = false;
if (isFinished) { return; }
if (spawnPos == null)
{
FindSpawnPosition(affectSubImmediately: true);
spawnPending = true;
}
bool spawnReady = false;
if (spawnPending)
@@ -218,21 +240,56 @@ namespace Barotrauma
{
if (submarine.IsOutpost) { continue; }
float minDist = GetMinDistanceToSub(submarine);
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos) < minDist * minDist) return;
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
}
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
//unnecessary monsters in places the players might never visit during the round
if (spawnPosType == Level.PositionType.Ruin ||
spawnPosType == Level.PositionType.Cave)
{
bool someoneNearby = false;
float minDist = Items.Components.Sonar.DefaultSonarRange * 0.8f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.IsOutpost) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
foreach (Character c in Character.CharacterList)
{
if (c == Character.Controlled || c.IsRemotePlayer)
{
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
}
if (!someoneNearby) { return; }
}
spawnPending = false;
//+1 because Range returns an integer less than the max value
int amount = Rand.Range(minAmount, maxAmount + 1, Rand.RandSync.Server);
int amount = Rand.Range(minAmount, maxAmount + 1);
monsters = new List<Character>();
float offsetAmount = spawnPosType == Level.PositionType.MainPath ? 1000 : 100;
for (int i = 0; i < amount; i++)
{
CoroutineManager.InvokeAfter(() =>
{
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
monsters.Add(Character.Create(characterFile, spawnPos + Rand.Vector(offsetAmount, Rand.RandSync.Server), i.ToString(), null, isClient, true, true));
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events.");
monsters.Add(Character.Create(speciesName, spawnPos.Value + Rand.Vector(offsetAmount), Level.Loaded.Seed + i.ToString(), null, false, true, true));
if (monsters.Count == amount)
{
spawnReady = true;
@@ -240,7 +297,7 @@ namespace Barotrauma
//otherwise it'll make the spawned characters act as a swarm
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
}
}, Rand.Range(0f, amount / 2, Rand.RandSync.Server));
}, Rand.Range(0f, amount / 2));
}
}
@@ -248,7 +305,7 @@ namespace Barotrauma
Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter);
#if CLIENT
if (Character.Controlled != null) targetEntity = (Entity)Character.Controlled;
if (Character.Controlled != null) { targetEntity = Character.Controlled; }
#endif
bool monstersDead = true;
@@ -265,7 +322,7 @@ namespace Barotrauma
}
}
if (monstersDead) Finished();
if (monstersDead) { Finished(); }
}
}
}
@@ -1,4 +1,6 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -30,6 +32,11 @@ namespace Barotrauma
{
this.prefab = prefab;
}
public virtual IEnumerable<ContentFile> GetFilesToPreload()
{
yield break;
}
public virtual void Init(bool affectSubImmediately)
{
@@ -25,6 +25,10 @@ namespace Barotrauma
//the events in this set are delayed if the current EventManager intensity is not between these values
public readonly float MinIntensity, MaxIntensity;
public readonly bool AllowAtStart;
public readonly bool PerRuin;
public readonly Dictionary<string, float> Commonness;
public readonly List<ScriptedEventPrefab> EventPrefabs;
@@ -39,7 +43,7 @@ namespace Barotrauma
private ScriptedEventSet(XElement element, string debugIdentifier)
{
DebugIdentifier = debugIdentifier;
DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier;
Commonness = new Dictionary<string, float>();
EventPrefabs = new List<ScriptedEventPrefab>();
ChildSets = new List<ScriptedEventSet>();
@@ -54,6 +58,9 @@ namespace Barotrauma
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
AllowAtStart = element.GetAttributeBool("allowatstart", false);
PerRuin = element.GetAttributeBool("perruin", false);
Commonness[""] = 1.0f;
foreach (XElement subElement in element.Elements())
{
@@ -63,7 +70,7 @@ namespace Barotrauma
Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f);
foreach (XElement overrideElement in subElement.Elements())
{
if (overrideElement.Name.ToString().ToLowerInvariant() == "override")
if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase))
{
string levelType = overrideElement.GetAttributeString("leveltype", "");
if (!Commonness.ContainsKey(levelType))
@@ -101,22 +108,22 @@ namespace Barotrauma
return;
}
foreach (string configFile in configFiles)
foreach (ContentFile configFile in configFiles)
{
XDocument doc = XMLExtensions.TryLoadXml(configFile);
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { continue; }
var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
if (doc.Root.IsOverride())
{
DebugConsole.NewMessage($"Overriding all random events using the file {configFile}", Color.Yellow);
DebugConsole.NewMessage($"Overriding all random events using the file {configFile.Path}", Color.Yellow);
List.Clear();
}
int i = 0;
foreach (XElement element in doc.Root.Elements())
{
if (element.Name.ToString().ToLowerInvariant() != "eventset") { continue; }
if (!element.Name.ToString().Equals("eventset", StringComparison.OrdinalIgnoreCase)) { continue; }
List.Add(new ScriptedEventSet(element, i.ToString()));
i++;
}
@@ -6,17 +6,28 @@ namespace Barotrauma.Extensions
{
public static class IEnumerableExtensions
{
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
/// <summary>
/// Randomizes the collection (using OrderBy) and returns it.
/// </summary>
public static IOrderedEnumerable<T> Randomize<T>(this IEnumerable<T> source, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
return new HashSet<T>(source);
return source.OrderBy(i => Rand.Value(randSync));
}
/// <summary>
/// Randomizes the collection.
/// Randomizes the list in place without creating a new collection, using a Fisher-Yates-based algorithm.
/// </summary>
public static IOrderedEnumerable<T> Randomize<T>(this IEnumerable<T> source)
public static void Shuffle<T>(this IList<T> list, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
return source.OrderBy(i => Rand.Range(0f, 1f));
int n = list.Count;
while (n > 1)
{
n--;
int k = Rand.Int(n + 1, randSync);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
public static T GetRandom<T>(this IEnumerable<T> source, Func<T, bool> predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced)
@@ -78,6 +89,11 @@ namespace Barotrauma.Extensions
return source.Count(predicate) > 1;
}
}
public static IEnumerable<T> ToEnumerable<T>(this T item)
{
yield return item;
}
// source: https://stackoverflow.com/questions/19237868/get-all-children-to-one-list-recursive-c-sharp
public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
@@ -37,11 +37,22 @@ namespace Barotrauma.Extensions
return new Point((int)(p.X / v.X), (int)(p.Y / v.Y));
}
/// <summary>
/// Negates the X and Y components.
/// </summary>
public static Point Inverse(this Point p)
{
return new Point(-p.X, -p.Y);
}
/// <summary>
/// Flips the X and Y components.
/// </summary>
public static Point Flip(this Point p)
{
return new Point(p.Y, p.X);
}
public static Point Clamp(this Point p, Point min, Point max)
{
return new Point(MathHelper.Clamp(p.X, min.X, max.X), MathHelper.Clamp(p.Y, min.Y, max.Y));

Some files were not shown because too many files have changed in this diff Show More