diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e1ee8e88ce37..2a8aac42969a 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -24,6 +24,11 @@
/Content.*/Forensics/ @ficcialfaint
+/Content.*/Trigger/ @slarticodefast
+
+/Content.*/Stunnable/ @Princess-Cheeseballs
+/Content.*/Nutrition/ @Princess-Cheeseballs
+
# SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @crazybrain23
diff --git a/.github/workflows/publish-testing.yml b/.github/workflows/publish-testing.yml
index 3d2a016e5acd..0f08c6656501 100644
--- a/.github/workflows/publish-testing.yml
+++ b/.github/workflows/publish-testing.yml
@@ -50,7 +50,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
- run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+ run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 7db985b2bcb4..01f78cc4054a 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -38,7 +38,7 @@ jobs:
run: dotnet build Content.Packaging -c Release --no-restore /m
- name: Package server
- run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+ run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml
index 393bd1b8e261..54352b1ac1ff 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -60,7 +60,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
- run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+ run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000000..d8f05bd108eb
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,575 @@
+{
+ "yaml.customTags": [
+ "!type:AIShuntActionEvent mapping",
+ "!type:AIUnShuntActionEvent mapping",
+ "!type:AbductorReturnToShipEvent mapping",
+ "!type:AccessWireAction mapping",
+ "!type:ActionAnatomicPanaceaEvent mapping",
+ "!type:ActionAnomalyPulseEvent mapping",
+ "!type:ActionAugmentedEyesightEvent mapping",
+ "!type:ActionBiodegradeEvent mapping",
+ "!type:ActionChameleonSkinEvent mapping",
+ "!type:ActionComponentChangeEvent mapping",
+ "!type:ActionEphedrineOverdoseEvent mapping",
+ "!type:ActionFleshmendEvent mapping",
+ "!type:ActionGunShootEvent mapping",
+ "!type:ActionHivemindAccessEvent mapping",
+ "!type:ActionLesserFormEvent mapping",
+ "!type:ActionSpacesuitEvent mapping",
+ "!type:ActivateImplantEvent mapping",
+ "!type:ActivateWireAction mapping",
+ "!type:ActiveHandComponentPrecondition mapping",
+ "!type:ActiveHandEntityPrecondition mapping",
+ "!type:ActiveHandFreePrecondition mapping",
+ "!type:AddComponentSpecial mapping",
+ "!type:AddContainer mapping",
+ "!type:AddFloatOperator mapping",
+ "!type:AddImplantSpecial mapping",
+ "!type:AddToSolutionReaction mapping",
+ "!type:AdjacentNode mapping",
+ "!type:AdjustAlert mapping",
+ "!type:AdjustReagent mapping",
+ "!type:AdjustTemperature mapping",
+ "!type:AdminLog mapping",
+ "!type:AiInteractWireAction mapping",
+ "!type:AiVisionWireAction mapping",
+ "!type:AirAlarmPanicWire mapping",
+ "!type:AlarmThresholdSetting mapping",
+ "!type:AlertLevelCondition mapping",
+ "!type:AllConditions mapping",
+ "!type:AllSelector mapping",
+ "!type:AllWiresCut mapping",
+ "!type:AltInteractOperator mapping",
+ "!type:AlwaysTrueRule mapping",
+ "!type:AmeShieldIntegrity mapping",
+ "!type:AmmoniaOxygenReaction mapping",
+ "!type:AnyConditions mapping",
+ "!type:AppearanceChange mapping",
+ "!type:ArcadeEnemyInvincibleWireAction mapping",
+ "!type:ArcadeOverflowWireAction mapping",
+ "!type:ArcadePlayerInvincibleWireAction mapping",
+ "!type:AreaReactionEffect mapping",
+ "!type:ArtifactDurabilityRestore mapping",
+ "!type:ArtifactSelfActivateEvent mapping",
+ "!type:ArtifactUnlock mapping",
+ "!type:AtmosMonitorDeviceNetWire mapping",
+ "!type:AttemptElectrocute mapping",
+ "!type:AutoCablingDunGen mapping",
+ "!type:BinomialNumberSelector mapping",
+ "!type:BiomeDecalLayer mapping",
+ "!type:BiomeDummyLayer mapping",
+ "!type:BiomeDunGen mapping",
+ "!type:BiomeEntityLayer mapping",
+ "!type:BiomeMarkerLoot mapping",
+ "!type:BiomeMetaLayer mapping",
+ "!type:BiomeTileLayer mapping",
+ "!type:BoardNodeEntity mapping",
+ "!type:BodyMindFilter mapping",
+ "!type:BoltWireAction mapping",
+ "!type:BooActionEvent mapping",
+ "!type:Bool mapping",
+ "!type:BoolCurve mapping",
+ "!type:BoomWireAction mapping",
+ "!type:BorgModuleActionSelectedEvent mapping",
+ "!type:BorgToggleSelectTypeEvent mapping",
+ "!type:BoundaryWallDunGen mapping",
+ "!type:BreakVowAlertEvent mapping",
+ "!type:Breathing mapping",
+ "!type:BuckledPrecondition mapping",
+ "!type:BuildMech mapping",
+ "!type:BurnBodyBehavior mapping",
+ "!type:BuyBeforeCondition mapping",
+ "!type:BuyerAccessCondition mapping",
+ "!type:BuyerDepartmentCondition mapping",
+ "!type:BuyerJobCondition mapping",
+ "!type:BuyerWhitelistCondition mapping",
+ "!type:CableDeviceNode mapping",
+ "!type:CableNode mapping",
+ "!type:CableTerminalNode mapping",
+ "!type:CableTerminalPortNode mapping",
+ "!type:CategoryColoring mapping",
+ "!type:CauseZombieInfection mapping",
+ "!type:ChameleonControllerOpenMenuEvent mapping",
+ "!type:ChangeComponentsSpellEvent mapping",
+ "!type:ChangeConstructionNodeBehavior mapping",
+ "!type:ChangelingDevourActionEvent mapping",
+ "!type:ChangelingTransformActionEvent mapping",
+ "!type:ChargeSpellEvent mapping",
+ "!type:ChemCleanBloodstream mapping",
+ "!type:ChemHealEyeDamage mapping",
+ "!type:ChemVomit mapping",
+ "!type:ChineseNewYear mapping",
+ "!type:CleanDecalsReaction mapping",
+ "!type:CleanTileReaction mapping",
+ "!type:ClearAllOverlaysEvent mapping",
+ "!type:ClothingSlotFilter mapping",
+ "!type:ColorCycleBehaviour mapping",
+ "!type:ComponentFilter mapping",
+ "!type:ComponentQuery mapping",
+ "!type:Computus mapping",
+ "!type:ConditionAlwaysMatch mapping",
+ "!type:ConditionManualBlacklistMembership mapping",
+ "!type:ConditionManualWhitelistMembership mapping",
+ "!type:ConditionNotesPlaytimeRange mapping",
+ "!type:ConditionPlayerCount mapping",
+ "!type:ConditionPlaytime mapping",
+ "!type:ConditionalAction mapping",
+ "!type:ConstantNumberSelector mapping",
+ "!type:Container mapping",
+ "!type:ContainerEmpty mapping",
+ "!type:ContainerNotEmpty mapping",
+ "!type:ContainerOperator mapping",
+ "!type:ContainerSlot mapping",
+ "!type:CoordinatesInRangePrecondition mapping",
+ "!type:CoordinatesNotInRangePrecondition mapping",
+ "!type:CornerClutterDunGen mapping",
+ "!type:CorridorClutterDunGen mapping",
+ "!type:CorridorDecalSkirtingDunGen mapping",
+ "!type:CorridorDunGen mapping",
+ "!type:CreateBoneShardEvent mapping",
+ "!type:CreateEntityReactionEffect mapping",
+ "!type:CreateEntityTileReaction mapping",
+ "!type:CreateGas mapping",
+ "!type:CreateItemEvent mapping",
+ "!type:CrewManifestUi mapping",
+ "!type:CritFakeDeathEvent mapping",
+ "!type:CritLastWordsEvent mapping",
+ "!type:CritSuccumbEvent mapping",
+ "!type:CryoPodEjectLockWireAction mapping",
+ "!type:CureZombieInfection mapping",
+ "!type:Custom mapping",
+ "!type:DamageDealCondition mapping",
+ "!type:DamageEntity mapping",
+ "!type:DamageGroupTrigger mapping",
+ "!type:DamageTrigger mapping",
+ "!type:DamageTypeTrigger mapping",
+ "!type:DashEvent mapping",
+ "!type:DayOfYear mapping",
+ "!type:DelayWireAction mapping",
+ "!type:DeleteEntitiesInContainer mapping",
+ "!type:DeleteEntity mapping",
+ "!type:DepartmentTimeRequirement mapping",
+ "!type:DestroyEntity mapping",
+ "!type:DevourActionEvent mapping",
+ "!type:DisguiseToggleAnchoredEvent mapping",
+ "!type:DisguiseToggleNoRotEvent mapping",
+ "!type:DoActsBehavior mapping",
+ "!type:DoorBoltLightWireAction mapping",
+ "!type:DoorBoltWireAction mapping",
+ "!type:DoorBolted mapping",
+ "!type:DoorSafetyWireAction mapping",
+ "!type:DoorTimingWireAction mapping",
+ "!type:DoorWelded mapping",
+ "!type:DragonSpawnRiftActionEvent mapping",
+ "!type:DrinkValueCon mapping",
+ "!type:DropOperator mapping",
+ "!type:Drunk mapping",
+ "!type:DumpCanisterBehavior mapping",
+ "!type:DumpRestockInventory mapping",
+ "!type:DunGenEuclideanSquaredDistance mapping",
+ "!type:DungeonEntranceDunGen mapping",
+ "!type:DungeonSpawnGroup mapping",
+ "!type:EMPScreamEvent mapping",
+ "!type:EggInjectionEvent mapping",
+ "!type:EggLayInstantActionEvent mapping",
+ "!type:EggsInjectCondition mapping",
+ "!type:EggsLayingEvent mapping",
+ "!type:EjectVendorItems mapping",
+ "!type:Electrocute mapping",
+ "!type:ElectrocutionNode mapping",
+ "!type:Emote mapping",
+ "!type:EmpReactionEffect mapping",
+ "!type:EmptyAllContainers mapping",
+ "!type:EmptyAllContainersBehaviour mapping",
+ "!type:EmptyContainer mapping",
+ "!type:EmptyContainersBehaviour mapping",
+ "!type:EmptyOrWindowValidInTile mapping",
+ "!type:EnterStasisEvent mapping",
+ "!type:EntityAnchored mapping",
+ "!type:EntityTableDunGen mapping",
+ "!type:EntranceFlankDunGen mapping",
+ "!type:EquipOperator mapping",
+ "!type:EscapeOperator mapping",
+ "!type:EvenHealthChange mapping",
+ "!type:EvolveEvent mapping",
+ "!type:ExitConsoleEvent mapping",
+ "!type:ExitStasisActionEvent mapping",
+ "!type:ExitStasisEvent mapping",
+ "!type:ExplodeBehavior mapping",
+ "!type:ExplosionReactionEffect mapping",
+ "!type:ExteriorDunGen mapping",
+ "!type:ExternalWindowDunGen mapping",
+ "!type:ExtinguishReaction mapping",
+ "!type:ExtinguishTileReaction mapping",
+ "!type:EyeColoring mapping",
+ "!type:FadeBehaviour mapping",
+ "!type:FakeMindShieldToggleEvent mapping",
+ "!type:FillGridDunGen mapping",
+ "!type:FireStarterActionEvent mapping",
+ "!type:FlammableReaction mapping",
+ "!type:FlammableTileReaction mapping",
+ "!type:FlashReactionEffect mapping",
+ "!type:FoodHasReagent mapping",
+ "!type:FoodValueCon mapping",
+ "!type:FrezonCoolantReaction mapping",
+ "!type:FrezonProductionReaction mapping",
+ "!type:FridayThirteenth mapping",
+ "!type:GeneratedParallaxTextureSource mapping",
+ "!type:GenericStatusEffect mapping",
+ "!type:GibActionEvent mapping",
+ "!type:GibBehavior mapping",
+ "!type:GiveItemOnHolidaySpecial mapping",
+ "!type:GivePrototype mapping",
+ "!type:GizmoMarkEvent mapping",
+ "!type:Glow mapping",
+ "!type:GoliathSummonTentacleAction mapping",
+ "!type:GravityJumpEvent mapping",
+ "!type:GridInRangeRule mapping",
+ "!type:GridSpawnGroup mapping",
+ "!type:GroupLoadoutEffect mapping",
+ "!type:GroupSelector mapping",
+ "!type:GuardianToggleActionEvent mapping",
+ "!type:GunAmmoPrecondition mapping",
+ "!type:GunOperator mapping",
+ "!type:HTNCompoundTask mapping",
+ "!type:HTNPrimitiveTask mapping",
+ "!type:HasOrdersPrecondition mapping",
+ "!type:HasRoleMindFilter mapping",
+ "!type:HasTag mapping",
+ "!type:HealthChange mapping",
+ "!type:Hunger mapping",
+ "!type:HungryPrecondition mapping",
+ "!type:Ignite mapping",
+ "!type:ImageParallaxTextureSource mapping",
+ "!type:InContainerPrecondition mapping",
+ "!type:InSpaceRule mapping",
+ "!type:IngredientsWithTags mapping",
+ "!type:InstantSpawnSpellEvent mapping",
+ "!type:InteractWithOperator mapping",
+ "!type:Internals mapping",
+ "!type:InventoryQuery mapping",
+ "!type:InverseBoolCurve mapping",
+ "!type:InvisibleWallActionEvent mapping",
+ "!type:ItemSwitchState mapping",
+ "!type:JetJumpActionEvent mapping",
+ "!type:Jitter mapping",
+ "!type:JobRequirementLoadoutEffect mapping",
+ "!type:JukeOperator mapping",
+ "!type:JumpActionEvent mapping",
+ "!type:JumpToCoreEvent mapping",
+ "!type:JunctionDunGen mapping",
+ "!type:KeyExistsPrecondition mapping",
+ "!type:KeyFloatLessPrecondition mapping",
+ "!type:KnockSpellEvent mapping",
+ "!type:KnockedDownAlertEvent mapping",
+ "!type:LastElementHasTags mapping",
+ "!type:ListenWireAction mapping",
+ "!type:ListingLimitedStockCondition mapping",
+ "!type:ListingPlayerCountCondition mapping",
+ "!type:Locked mapping",
+ "!type:LogProbeUi mapping",
+ "!type:LogWireAction mapping",
+ "!type:MachineDeconstructedEvent mapping",
+ "!type:MachineFrameComplete mapping",
+ "!type:MachineFrameRegenerateProgress mapping",
+ "!type:MakeSentient mapping",
+ "!type:MechEjectPilotEvent mapping",
+ "!type:MechGrabberUi mapping",
+ "!type:MechOpenUiEvent mapping",
+ "!type:MechSoundboardUi mapping",
+ "!type:MechToggleEquipmentEvent mapping",
+ "!type:MechToggleInternalsEvent mapping",
+ "!type:MechToggleNightVisionEvent mapping",
+ "!type:MechToggleSirensEvent mapping",
+ "!type:MechToggleThrustersEvent mapping",
+ "!type:MedibotInjectOperator mapping",
+ "!type:MeleeOperator mapping",
+ "!type:MeleeSpeechConfigureActionEvent mapping",
+ "!type:MinSolution mapping",
+ "!type:MindSwapSpellEvent mapping",
+ "!type:MobStateCondition mapping",
+ "!type:MobsDunGen mapping",
+ "!type:ModifyBleedAmount mapping",
+ "!type:ModifyBloodLevel mapping",
+ "!type:ModifyLungGas mapping",
+ "!type:ModifyStatusEffect mapping",
+ "!type:MoveToOperator mapping",
+ "!type:MovespeedModifier mapping",
+ "!type:N2ODecompositionReaction mapping",
+ "!type:NanoTaskUi mapping",
+ "!type:NanotrasenNameGenerator mapping",
+ "!type:NearbyAccessRule mapping",
+ "!type:NearbyComponentsRule mapping",
+ "!type:NearbyEntitiesRule mapping",
+ "!type:NearbyHostilesQuery mapping",
+ "!type:NearbyTilesPercentRule mapping",
+ "!type:NestedSelector mapping",
+ "!type:NetProbeUi mapping",
+ "!type:NewsReaderUi mapping",
+ "!type:NinjaEmpEvent mapping",
+ "!type:NoOperator mapping",
+ "!type:NoUnstackableInTile mapping",
+ "!type:NoWindowsInTile mapping",
+ "!type:NoiseDistanceDunGen mapping",
+ "!type:NoiseDunGen mapping",
+ "!type:NotekeeperUi mapping",
+ "!type:NullPhaseActionEvent mapping",
+ "!type:ObjectiveMindFilter mapping",
+ "!type:OfferItemAlertEvent mapping",
+ "!type:OnItemRecallActionEvent mapping",
+ "!type:OnMapGridRule mapping",
+ "!type:OnRetractableItemActionEvent mapping",
+ "!type:OpenBehavior mapping",
+ "!type:OpenCardsAlertEvent mapping",
+ "!type:OpenEvolutionMenuEvent mapping",
+ "!type:OpenStorageImplantEvent mapping",
+ "!type:OpenUiActionEvent mapping",
+ "!type:OpenUplinkImplantEvent mapping",
+ "!type:OrderedTargetCon mapping",
+ "!type:OreDunGen mapping",
+ "!type:OrganType mapping",
+ "!type:OverallPlaytimeRequirement mapping",
+ "!type:Oxygenate mapping",
+ "!type:PAIShopActionEvent mapping",
+ "!type:ParticleAcceleratorKeyboardWireAction mapping",
+ "!type:ParticleAcceleratorLimiterWireAction mapping",
+ "!type:ParticleAcceleratorPowerWireAction mapping",
+ "!type:ParticleAcceleratorStrengthWireAction mapping",
+ "!type:PhysShapeAabb mapping",
+ "!type:PhysShapeCircle mapping",
+ "!type:PickAccessibleOperator mapping",
+ "!type:PickNearbyInjectableOperator mapping",
+ "!type:PickPathfindPointOperator mapping",
+ "!type:PickRandomRotationOperator mapping",
+ "!type:PipeNode mapping",
+ "!type:PlantAdjustHealth mapping",
+ "!type:PlantAdjustMutationLevel mapping",
+ "!type:PlantAdjustMutationMod mapping",
+ "!type:PlantAdjustNutrition mapping",
+ "!type:PlantAdjustPests mapping",
+ "!type:PlantAdjustPotency mapping",
+ "!type:PlantAdjustToxins mapping",
+ "!type:PlantAdjustWater mapping",
+ "!type:PlantAdjustWeeds mapping",
+ "!type:PlantAffectGrowth mapping",
+ "!type:PlantChangeStat mapping",
+ "!type:PlantCryoxadone mapping",
+ "!type:PlantDiethylamine mapping",
+ "!type:PlantMutateChemicals mapping",
+ "!type:PlantMutateConsumeGasses mapping",
+ "!type:PlantMutateExudeGasses mapping",
+ "!type:PlantMutateHarvest mapping",
+ "!type:PlantPhalanximine mapping",
+ "!type:PlantRestoreSeeds mapping",
+ "!type:PlantSpeciesChange mapping",
+ "!type:PlasmaFireReaction mapping",
+ "!type:PlaySound mapping",
+ "!type:PlaySoundBehavior mapping",
+ "!type:PlayerCountCondition mapping",
+ "!type:PolygonShape mapping",
+ "!type:Polymorph mapping",
+ "!type:PolymorphActionEvent mapping",
+ "!type:PopupEveryone mapping",
+ "!type:PopupMessage mapping",
+ "!type:PortPipeNode mapping",
+ "!type:PortablePipeNode mapping",
+ "!type:PowerWireAction mapping",
+ "!type:PrefabDunGen mapping",
+ "!type:PrepareStasisActionEvent mapping",
+ "!type:PresetCurve mapping",
+ "!type:ProceedWireAction mapping",
+ "!type:ProjectileSpellEvent mapping",
+ "!type:PrototypeDunGen mapping",
+ "!type:PryTileReaction mapping",
+ "!type:PuddleFilter mapping",
+ "!type:PulledPrecondition mapping",
+ "!type:PulseBehaviour mapping",
+ "!type:QuadraticCurve mapping",
+ "!type:RaiseEvent mapping",
+ "!type:RandomGlobalSpawnSpellEvent mapping",
+ "!type:RandomObfuscation mapping",
+ "!type:RandomOperator mapping",
+ "!type:RandomSpawnsLoot mapping",
+ "!type:RandomizeBehaviour mapping",
+ "!type:RangeNumberSelector mapping",
+ "!type:RatKingDomainActionEvent mapping",
+ "!type:RatKingOrderActionEvent mapping",
+ "!type:RatKingRaiseArmyActionEvent mapping",
+ "!type:ReagentThreshold mapping",
+ "!type:RecallKatanaEvent mapping",
+ "!type:ReduceRotting mapping",
+ "!type:ReformEvent mapping",
+ "!type:RelayedActionComponentChangeEvent mapping",
+ "!type:RemoveAnchoredFilter mapping",
+ "!type:RemoveCuffsAlertEvent mapping",
+ "!type:RemoveEnsnareAlertEvent mapping",
+ "!type:ReplaceTileDunGen mapping",
+ "!type:ReplacementObfuscation mapping",
+ "!type:RepulseAttractActionEvent mapping",
+ "!type:ResetNarcolepsy mapping",
+ "!type:ResistFireAlertEvent mapping",
+ "!type:RetakeVowAlertEvent mapping",
+ "!type:RevenantBlightActionEvent mapping",
+ "!type:RevenantDefileActionEvent mapping",
+ "!type:RevenantMalfunctionActionEvent mapping",
+ "!type:RevenantOverloadLightsActionEvent mapping",
+ "!type:RevenantShopActionEvent mapping",
+ "!type:RevertPolymorphActionEvent mapping",
+ "!type:RngGhostRoleRaffleDecider mapping",
+ "!type:RobustHarvest mapping",
+ "!type:RoleTimeRequirement mapping",
+ "!type:RolesRequirement mapping",
+ "!type:RoomEntranceDunGen mapping",
+ "!type:RotateToTargetOperator mapping",
+ "!type:SatiateHunger mapping",
+ "!type:SatiateThirst mapping",
+ "!type:SayKeyOperator mapping",
+ "!type:ScreamActionEvent mapping",
+ "!type:SendYourselfEvent mapping",
+ "!type:SequenceLength mapping",
+ "!type:SericultureActionEvent mapping",
+ "!type:SetAnchor mapping",
+ "!type:SetFloatOperator mapping",
+ "!type:SetRandomFloatOperator mapping",
+ "!type:SetStackCount mapping",
+ "!type:SetWiresPanelSecurity mapping",
+ "!type:ShadekinAlertEvent mapping",
+ "!type:ShriekDissonantEvent mapping",
+ "!type:ShriekResonantEvent mapping",
+ "!type:SimpleColoring mapping",
+ "!type:Single mapping",
+ "!type:SkinColoring mapping",
+ "!type:SleepActionEvent mapping",
+ "!type:Slipify mapping",
+ "!type:SmiteSpellEvent mapping",
+ "!type:SnapToGrid mapping",
+ "!type:SolutionEmpty mapping",
+ "!type:SolutionExplosionBehavior mapping",
+ "!type:SoundCollectionSpecifier mapping",
+ "!type:SoundPathSpecifier mapping",
+ "!type:SpawnEntitiesBehavior mapping",
+ "!type:SpawnGasBehavior mapping",
+ "!type:SpawnItemInHandEvent mapping",
+ "!type:SpawnPrototype mapping",
+ "!type:SpawnPrototypeAtContainer mapping",
+ "!type:SpeakOperator mapping",
+ "!type:SpeciesLoadoutEffect mapping",
+ "!type:SpeechWireAction mapping",
+ "!type:SpiderWebActionEvent mapping",
+ "!type:SpiderWebCondition mapping",
+ "!type:SpillBehavior mapping",
+ "!type:SpillIfPuddlePresentTileReaction mapping",
+ "!type:SpillTileReaction mapping",
+ "!type:SplineDungeonConnectorDunGen mapping",
+ "!type:StartPlacementActionEvent mapping",
+ "!type:StashActiveHandOperator mapping",
+ "!type:StethoscopeActionEvent mapping",
+ "!type:StingBlindEvent mapping",
+ "!type:StingChemEvent mapping",
+ "!type:StingExtractDNAEvent mapping",
+ "!type:StingFakeArmbladeEvent mapping",
+ "!type:StingTransformEvent mapping",
+ "!type:StockLimitedListingCondition mapping",
+ "!type:StopBeingPulledAlertEvent mapping",
+ "!type:StopPilotingAlertEvent mapping",
+ "!type:StopPullingAlertEvent mapping",
+ "!type:StorageWelded mapping",
+ "!type:StoreWhitelistCondition mapping",
+ "!type:String mapping",
+ "!type:SubjectMindJobCondition mapping",
+ "!type:SubjectSpeciesCondition mapping",
+ "!type:SummonActionEvent mapping",
+ "!type:SwapToFreeHandOperator mapping",
+ "!type:SyllableObfuscation mapping",
+ "!type:TabletopBackgammonSetup mapping",
+ "!type:TabletopCheckerSetup mapping",
+ "!type:TabletopChessSetup mapping",
+ "!type:TabletopEmptySetup mapping",
+ "!type:TabletopParchisSetup mapping",
+ "!type:TargetAccessibleCon mapping",
+ "!type:TargetAmmoCon mapping",
+ "!type:TargetAmmoMatchesCon mapping",
+ "!type:TargetDistanceCon mapping",
+ "!type:TargetHealthCon mapping",
+ "!type:TargetInFront mapping",
+ "!type:TargetInLOSOrCurrentCon mapping",
+ "!type:TargetInLOSPrecondition mapping",
+ "!type:TargetInRangePrecondition mapping",
+ "!type:TargetIsAliveCon mapping",
+ "!type:TargetIsCritCon mapping",
+ "!type:TargetIsStunnedCon mapping",
+ "!type:TargetLowTempCon mapping",
+ "!type:TargetMeleeCon mapping",
+ "!type:TargetObjectiveMindFilter mapping",
+ "!type:TargetOnFireCon mapping",
+ "!type:TattooColoring mapping",
+ "!type:TegNodeCirculator mapping",
+ "!type:TegNodeGenerator mapping",
+ "!type:TeleportSpellEvent mapping",
+ "!type:Temperature mapping",
+ "!type:ThirstyPrecondition mapping",
+ "!type:TileNotBlocked mapping",
+ "!type:TileType mapping",
+ "!type:TimerStartBehavior mapping",
+ "!type:ToggleActionEvent mapping",
+ "!type:ToggleChitinousArmorEvent mapping",
+ "!type:ToggleClothingEvent mapping",
+ "!type:ToggleCombatActionEvent mapping",
+ "!type:ToggleEyesActionEvent mapping",
+ "!type:ToggleFoVActionEvent mapping",
+ "!type:ToggleGhostHearingActionEvent mapping",
+ "!type:ToggleGhostVisibilityToAllEvent mapping",
+ "!type:ToggleGhostsActionEvent mapping",
+ "!type:ToggleInternalsAlertEvent mapping",
+ "!type:ToggleIntrinsicUIEvent mapping",
+ "!type:ToggleJetpackEvent mapping",
+ "!type:ToggleLawsScreenEvent mapping",
+ "!type:ToggleLightingActionEvent mapping",
+ "!type:ToggleLimbEvent mapping",
+ "!type:ToggleMaskEvent mapping",
+ "!type:ToggleStrainedMusclesEvent mapping",
+ "!type:ToggleThermalVisionEvent mapping",
+ "!type:ToggleThievingEvent mapping",
+ "!type:TotalDamage mapping",
+ "!type:TransformMech mapping",
+ "!type:TriggerBehavior mapping",
+ "!type:TritiumFireReaction mapping",
+ "!type:TurretTargetingCon mapping",
+ "!type:UnPullOperator mapping",
+ "!type:UnbuckleAlertEvent mapping",
+ "!type:UnbuckleOperator mapping",
+ "!type:UseDelayInfo mapping",
+ "!type:UtilityOperator mapping",
+ "!type:UtilityService mapping",
+ "!type:VampireBloodStealEvent mapping",
+ "!type:VampireCloakOfDarknessEvent mapping",
+ "!type:VampireGlareEvent mapping",
+ "!type:VampireHypnotiseEvent mapping",
+ "!type:VampireOpenMutationsMenu mapping",
+ "!type:VampirePolymorphEvent mapping",
+ "!type:VampireScreechEvent mapping",
+ "!type:VampireSupernaturalStrengthEvent mapping",
+ "!type:VampireToggleFangsEvent mapping",
+ "!type:VampireUnholyStrengthEvent mapping",
+ "!type:VendingMachineContrabandWireAction mapping",
+ "!type:VendingMachineEjectItemWireAction mapping",
+ "!type:VendingMachineSelfDispenseEvent mapping",
+ "!type:VisualizerDataInt mapping",
+ "!type:VoiceMaskSetNameEvent mapping",
+ "!type:VoidApplauseSpellEvent mapping",
+ "!type:WaitOperator mapping",
+ "!type:WakeActionEvent mapping",
+ "!type:WallMountDunGen mapping",
+ "!type:WallmountCondition mapping",
+ "!type:WantedListUi mapping",
+ "!type:WashCreamPieReaction mapping",
+ "!type:WaterVaporReaction mapping",
+ "!type:WearableReaction mapping",
+ "!type:WeekdayInMonth mapping",
+ "!type:WeightedSpawnEntityBehavior mapping",
+ "!type:WirePanel mapping",
+ "!type:WorldSpawnSpellEvent mapping",
+ "!type:WormCorridorDunGen mapping",
+ "!type:ZombifySelfActionEvent mapping"
+ ]
+}
\ No newline at end of file
diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py
index 96a7bdae2ab1..66d2463669db 100644
--- a/BuildChecker/git_helper.py
+++ b/BuildChecker/git_helper.py
@@ -1,17 +1,19 @@
#!/usr/bin/env python3
-# Installs git hooks, updates them, updates submodules, that kind of thing.
+"""
+Installs git hooks, updates them, updates submodules, that kind of thing.
+"""
-import subprocess
-import sys
import os
import shutil
+import subprocess
+import sys
import time
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
# If this doesn't match the saved version we overwrite them all.
-CURRENT_HOOKS_VERSION = "2"
+CURRENT_HOOKS_VERSION = "3"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
@@ -25,12 +27,10 @@ def run_command(command: List[str], capture: bool = False) -> subprocess.Complet
sys.stdout.flush()
- completed = None
-
if capture:
- completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE)
+ completed = subprocess.run(command, stdout=subprocess.PIPE, text=True)
else:
- completed = subprocess.run(command, cwd="..")
+ completed = subprocess.run(command)
if completed.returncode != 0:
print("Error: command exited with code {}!".format(completed.returncode))
@@ -43,7 +43,7 @@ def update_submodules():
Updates all submodules.
"""
- if ('GITHUB_ACTIONS' in os.environ):
+ if 'GITHUB_ACTIONS' in os.environ:
return
if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"):
@@ -76,22 +76,21 @@ def install_hooks():
print("No hooks change detected.")
return
- with open("INSTALLED_HOOKS_VERSION", "w") as f:
- f.write(CURRENT_HOOKS_VERSION)
-
print("Hooks need updating.")
- hooks_target_dir = Path("..")/".git"/"hooks"
+ hooks_target_dir = Path(run_command(["git", "rev-parse", "--git-path", "hooks"], True).stdout.strip())
hooks_source_dir = Path("hooks")
# Clear entire tree since we need to kill deleted files too.
- for filename in os.listdir(str(hooks_target_dir)):
- os.remove(str(hooks_target_dir/filename))
+ for filename in os.listdir(hooks_target_dir):
+ os.remove(hooks_target_dir / filename)
- for filename in os.listdir(str(hooks_source_dir)):
+ for filename in os.listdir(hooks_source_dir):
print("Copying hook {}".format(filename))
- shutil.copy2(str(hooks_source_dir/filename),
- str(hooks_target_dir/filename))
+ shutil.copy2(hooks_source_dir / filename, hooks_target_dir / filename)
+
+ with open("INSTALLED_HOOKS_VERSION", "w") as f:
+ f.write(CURRENT_HOOKS_VERSION)
def reset_solution():
@@ -107,8 +106,7 @@ def reset_solution():
def check_for_zip_download():
# Check if .git exists,
- cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
- if not os.path.isdir(os.path.join(cur_dir, ".git")):
+ if run_command(["git", "rev-parse"]).returncode != 0:
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
"Such as information to download the engine or even the ability to even be able to create contributions. \n"
diff --git a/BuildChecker/hooks/post-checkout b/BuildChecker/hooks/post-checkout
index c5662445c272..ee4309de1dad 100755
--- a/BuildChecker/hooks/post-checkout
+++ b/BuildChecker/hooks/post-checkout
@@ -1,10 +1,10 @@
#!/bin/bash
-gitroot=`git rev-parse --show-toplevel`
+gitroot=$(git rev-parse --show-toplevel)
-cd "$gitroot/BuildChecker"
+cd "$gitroot/BuildChecker" || exit
-if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
+if [[ $(uname) == MINGW* || $(uname) == CYGWIN* ]]; then
# Windows
py -3 git_helper.py --quiet
else
diff --git a/BuildChecker/hooks/post-merge b/BuildChecker/hooks/post-merge
index 85fe61d966c4..5cf3d91120d5 100755
--- a/BuildChecker/hooks/post-merge
+++ b/BuildChecker/hooks/post-merge
@@ -1,5 +1,5 @@
#!/bin/bash
# Just call post-checkout since it does the same thing.
-gitroot=`git rev-parse --show-toplevel`
-bash "$gitroot/.git/hooks/post-checkout"
+gitroot=$(git rev-parse --git-path hooks)
+bash "$gitroot/post-checkout"
diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs
new file mode 100644
index 000000000000..b31b3ed1a247
--- /dev/null
+++ b/Content.Benchmarks/DeltaPressureBenchmark.cs
@@ -0,0 +1,174 @@
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos.Components;
+using Content.Shared.CCVar;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Benchmarks;
+
+///
+/// Spawns N number of entities with a and
+/// simulates them for a number of ticks M.
+///
+[Virtual]
+[GcServer(true)]
+//[MemoryDiagnoser]
+//[ThreadingDiagnoser]
+public class DeltaPressureBenchmark
+{
+ ///
+ /// Number of entities (windows, really) to spawn with a .
+ ///
+ [Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
+ public int EntityCount;
+
+ ///
+ /// Number of entities that each parallel processing job will handle.
+ ///
+ // [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN)
+ [Params(10)]
+ public int BatchSize;
+
+ ///
+ /// Number of entities to process per iteration in the DeltaPressure
+ /// processing loop.
+ ///
+ // [Params(100, 1000, 5000, 10000, 50000)]
+ [Params(1000)]
+ public int EntitiesPerIteration;
+
+ private readonly EntProtoId _windowProtoId = "Window";
+ private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible";
+
+ private TestPair _pair = default!;
+ private IEntityManager _entMan = default!;
+ private SharedMapSystem _map = default!;
+ private IRobustRandom _random = default!;
+ private IConfigurationManager _cvar = default!;
+ private ITileDefinitionManager _tileDefMan = default!;
+ private AtmosphereSystem _atmospereSystem = default!;
+
+ private Entity
+ _testEnt;
+
+ [GlobalSetup]
+ public async Task SetupAsync()
+ {
+ ProgramShared.PathOffset = "../../../../";
+ PoolManager.Startup();
+ _pair = await PoolManager.GetServerClient();
+ var server = _pair.Server;
+
+ var mapdata = await _pair.CreateTestMap();
+
+ _entMan = server.ResolveDependency();
+ _map = _entMan.System();
+ _random = server.ResolveDependency();
+ _cvar = server.ResolveDependency();
+ _tileDefMan = server.ResolveDependency();
+ _atmospereSystem = _entMan.System();
+
+ _random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
+
+ _cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration);
+ _cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize);
+
+ var plating = _tileDefMan["Plating"].TileId;
+
+ /*
+ Basically, we want to have a 5-wide grid of tiles.
+ Edges are walled, and the length of the grid is determined by N + 2.
+ Windows should only touch the top and bottom walls, and each other.
+ */
+
+ var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls
+ const int height = 5;
+
+ await server.WaitPost(() =>
+ {
+ // Fill required tiles (extend grid) with plating
+ for (var x = 0; x < length; x++)
+ {
+ for (var y = 0; y < height; y++)
+ {
+ _map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
+ }
+ }
+
+ // Spawn perimeter walls and windows row in the middle (y = 2)
+ const int midY = height / 2;
+ for (var x = 0; x < length; x++)
+ {
+ for (var y = 0; y < height; y++)
+ {
+ var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
+
+ var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1;
+ if (isPerimeter)
+ {
+ _entMan.SpawnEntity(_wallProtoId, coords);
+ continue;
+ }
+
+ // Spawn windows only on the middle row, spanning interior (excluding side walls)
+ if (y == midY)
+ {
+ _entMan.SpawnEntity(_windowProtoId, coords);
+ }
+ }
+ }
+ });
+
+ // Next we run the fixgridatmos command to ensure that we have some air on our grid.
+ // Wait a little bit as well.
+ // TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just
+ // a stamp-on in AtmosphereSystem.
+ await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1);
+
+ var uid = mapdata.Grid.Owner;
+ _testEnt = new Entity(
+ uid,
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid));
+ }
+
+ [Benchmark]
+ public async Task PerformFullProcess()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { }
+ });
+ }
+
+ [Benchmark]
+ public async Task PerformSingleRunProcess()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ _atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure);
+ });
+ }
+
+ [GlobalCleanup]
+ public async Task CleanupAsync()
+ {
+ await _pair.DisposeAsync();
+ PoolManager.Shutdown();
+ }
+}
diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index de788234e5cf..3d527953b831 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -47,7 +47,7 @@ public async Task Cleanup()
PoolManager.Shutdown();
}
- public static readonly string[] MapsSource = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
+ public static string[] MapsSource { get; } = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
[ParamsSource(nameof(MapsSource))]
public string Map;
diff --git a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
index 092a0071fb70..8e527d7343a3 100644
--- a/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
+++ b/Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
@@ -29,7 +29,7 @@ public void SetAccessLevels(IPrototypeManager protoManager, List> newAccessList, string newJobPrototype)
+ public void SubmitData(string newFullName, string newJobTitle, List> newAccessList, ProtoId newJobPrototype)
{
if (newFullName.Length > _maxNameLength)
newFullName = newFullName[.._maxNameLength];
diff --git a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
index 3b19699d3ac7..9e1672f00dad 100644
--- a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
+++ b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
@@ -123,7 +123,7 @@ private void SelectJobPreset(OptionButton.ItemSelectedEventArgs args)
foreach (var group in job.AccessGroups)
{
- if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
+ if (!_prototypeManager.Resolve(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}
diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs
index a62097316b5c..ef55b77f49a0 100644
--- a/Content.Client/Administration/Managers/ClientAdminManager.cs
+++ b/Content.Client/Administration/Managers/ClientAdminManager.cs
@@ -16,6 +16,7 @@ public sealed class ClientAdminManager : IClientAdminManager, IClientConGroupImp
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IClientNetManager _netMgr = default!;
[Dependency] private readonly IClientConGroupController _conGroup = default!;
+ [Dependency] private readonly IClientConsoleHost _host = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
@@ -87,12 +88,12 @@ public void Initialize()
private void UpdateMessageRx(MsgUpdateAdminStatus message)
{
_availableCommands.Clear();
- var host = IoCManager.Resolve();
// Anything marked as Any we'll just add even if the server doesn't know about it.
- foreach (var (command, instance) in host.AvailableCommands)
+ foreach (var (command, instance) in _host.AvailableCommands)
{
- if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) continue;
+ if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null)
+ continue;
_availableCommands.Add(command);
}
diff --git a/Content.Client/Administration/Systems/AdminFrozenSystem.cs b/Content.Client/Administration/Systems/AdminFrozenSystem.cs
deleted file mode 100644
index 885585f985ce..000000000000
--- a/Content.Client/Administration/Systems/AdminFrozenSystem.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Administration;
-
-namespace Content.Client.Administration.Systems;
-
-public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
-{
-}
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
index 46090a6f3d04..c7aed30c1cdf 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
@@ -316,8 +316,9 @@ private void AddRoleCheckbox(string group, string role, GridContainer roleGroupI
};
// This is adding the icon before the role name
- // Yeah, this is sus, but having to split the functions up and stuff is worse imo.
- if (_protoMan.TryIndex(role, out var jobPrototype) && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
+ // TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
+ // I know the ban manager is doing the same thing, but that should not leak into UI code.
+ if (_protoMan.TryIndex(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto))
{
var jobIconTexture = new TextureRect
{
diff --git a/Content.Client/Administration/UI/CustomControls/HSeparator.cs b/Content.Client/Administration/UI/CustomControls/HSeparator.cs
index 91828c4f170a..9a7e30ef2dda 100644
--- a/Content.Client/Administration/UI/CustomControls/HSeparator.cs
+++ b/Content.Client/Administration/UI/CustomControls/HSeparator.cs
@@ -9,6 +9,7 @@ public sealed class HSeparator : Control
{
private static readonly Color SeparatorColor = Color.FromHex("#3D4059");
+ // Starlight-start
private readonly StyleBoxFlat _styleBox;
public Color Color
@@ -16,9 +17,11 @@ public Color Color
get => _styleBox.BackgroundColor;
set => _styleBox.BackgroundColor = value;
}
+ // Starlight-end
public HSeparator(Color color)
{
+ // Starlight-start
_styleBox = new StyleBoxFlat
{
BackgroundColor = color,
@@ -27,6 +30,7 @@ public HSeparator(Color color)
};
AddChild(new PanelContainer { PanelOverride = _styleBox });
+ // Starlight-end
}
public HSeparator() : this(SeparatorColor) { }
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
index de9ccbbf50ce..fa92a3e18fb7 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
@@ -57,12 +57,43 @@ public ObjectsTab()
private void TeleportTo(NetEntity nent)
{
- _console.ExecuteCommand($"tpto {nent}");
+ var selection = _selections[ObjectTypeOptions.SelectedId];
+ switch (selection)
+ {
+ case ObjectsTabSelection.Grids:
+ {
+ // directly teleport to the entity
+ _console.ExecuteCommand($"tpto {nent}");
+ }
+ break;
+ case ObjectsTabSelection.Maps:
+ {
+ // teleport to the map, not to the map entity (which is in nullspace)
+ if (!_entityManager.TryGetEntity(nent, out var map) || !_entityManager.TryGetComponent(map, out var mapComp))
+ break;
+ _console.ExecuteCommand($"tp 0 0 {mapComp.MapId}");
+ break;
+ }
+ case ObjectsTabSelection.Stations:
+ {
+ // teleport to the station's largest grid, not to the station entity (which is in nullspace)
+ if (!_entityManager.TryGetEntity(nent, out var station))
+ break;
+ var largestGrid = _entityManager.EntitySysManager.GetEntitySystem().GetLargestGrid(station.Value);
+ if (largestGrid == null)
+ break;
+ _console.ExecuteCommand($"tpto {largestGrid.Value}");
+ break;
+ }
+ default:
+ throw new NotImplementedException();
+ }
}
private void Delete(NetEntity nent)
{
_console.ExecuteCommand($"delete {nent}");
+ RefreshObjectList();
}
public void RefreshObjectList()
@@ -79,25 +110,21 @@ private void RefreshObjectList(ObjectsTabSelection selection)
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem().GetStationNames());
break;
case ObjectsTabSelection.Grids:
- {
- var query = _entityManager.AllEntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var metadata))
{
- entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- }
+ var query = _entityManager.AllEntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _, out var metadata))
+ entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- break;
- }
+ break;
+ }
case ObjectsTabSelection.Maps:
- {
- var query = _entityManager.AllEntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var metadata))
{
- entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- }
+ var query = _entityManager.AllEntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _, out var metadata))
+ entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- break;
- }
+ break;
+ }
default:
throw new ArgumentOutOfRangeException(nameof(selection), selection, null);
}
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
index c561125a30c7..7c9dde79b582 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
@@ -1,5 +1,6 @@
-
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
index 66d875b0f233..f6688a63af4f 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
@@ -11,6 +11,7 @@
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
namespace Content.Client.Lathe.UI;
@@ -26,6 +27,10 @@ public sealed partial class LatheMenu : DefaultWindow
public event Action? OnServerListButtonPressed;
public event Action? RecipeQueueAction;
+ public event Action? QueueDeleteAction;
+ public event Action? QueueMoveUpAction;
+ public event Action? QueueMoveDownAction;
+ public event Action? DeleteFabricatingAction;
public List> Recipes = new();
@@ -50,12 +55,21 @@ public LatheMenu()
};
AmountLineEdit.OnTextChanged += _ =>
{
+ if (int.TryParse(AmountLineEdit.Text, out var amount))
+ {
+ if (amount > LatheSystem.MaxItemsPerRequest)
+ AmountLineEdit.Text = LatheSystem.MaxItemsPerRequest.ToString();
+ else if (amount < 0)
+ AmountLineEdit.Text = "0";
+ }
+
PopulateRecipes();
};
FilterOption.OnItemSelected += OnItemSelected;
ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a);
+ DeleteFabricating.OnPressed += _ => DeleteFabricatingAction?.Invoke();
}
public void SetEntity(EntityUid uid)
@@ -83,7 +97,7 @@ public void PopulateRecipes()
var recipesToShow = new List();
foreach (var recipe in Recipes)
{
- if (!_prototypeManager.TryIndex(recipe, out var proto))
+ if (!_prototypeManager.Resolve(recipe, out var proto))
continue;
// Category filtering
@@ -115,21 +129,50 @@ public void PopulateRecipes()
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
- RecipeList.Children.Clear();
+
+ // Get the existing list of queue controls
+ var oldChildCount = RecipeList.ChildCount;
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
+ int idx = 0;
foreach (var prototype in sortedRecipesToShow)
{
var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe);
+ var tooltipFunction = () => GenerateTooltipText(prototype);
- var control = new RecipeControl(_lathe, prototype, () => GenerateTooltipText(prototype), canProduce, GetRecipeDisplayControl(prototype));
- control.OnButtonPressed += s =>
+ if (idx >= oldChildCount)
+ {
+ var control = new RecipeControl(_lathe, prototype, tooltipFunction, canProduce, GetRecipeDisplayControl(prototype));
+ control.OnButtonPressed += s =>
+ {
+ if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
+ amount = 1;
+ RecipeQueueAction?.Invoke(s, amount);
+ };
+ RecipeList.AddChild(control);
+ }
+ else
{
- if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
- amount = 1;
- RecipeQueueAction?.Invoke(s, amount);
- };
- RecipeList.AddChild(control);
+ var child = RecipeList.GetChild(idx) as RecipeControl;
+
+ if (child == null)
+ {
+ DebugTools.Assert($"Lathe menu recipe control at {idx} is not of type RecipeControl"); // Something's gone terribly wrong.
+ continue;
+ }
+
+ child.SetRecipe(prototype);
+ child.SetTooltipSupplier(tooltipFunction);
+ child.SetCanProduce(canProduce);
+ child.SetDisplayControl(GetRecipeDisplayControl(prototype));
+ }
+ idx++;
+ }
+
+ // Shrink list if new list is shorter than old list.
+ for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
+ {
+ RecipeList.RemoveChild(childIdx);
}
}
@@ -140,7 +183,7 @@ private string GenerateTooltipText(LatheRecipePrototype prototype)
foreach (var (id, amount) in prototype.Materials)
{
- if (!_prototypeManager.TryIndex(id, out var proto))
+ if (!_prototypeManager.Resolve(id, out var proto))
continue;
var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, multiplier);
@@ -223,25 +266,53 @@ public void UpdateCategories()
/// Populates the build queue list with all queued items
///
///
- public void PopulateQueueList(IReadOnlyCollection> queue)
+ public void PopulateQueueList(IReadOnlyCollection queue)
{
- QueueList.DisposeAllChildren();
+ // Get the existing list of queue controls
+ var oldChildCount = QueueList.ChildCount;
- var idx = 1;
- foreach (var recipeProto in queue)
+ var idx = 0;
+ foreach (var batch in queue)
{
- var recipe = _prototypeManager.Index(recipeProto);
- var queuedRecipeBox = new BoxContainer();
- queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
+ var recipe = _prototypeManager.Index(batch.Recipe);
+
+ var itemName = _lathe.GetRecipeName(batch.Recipe);
+ string displayText;
+ if (batch.ItemsRequested > 1)
+ displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx + 1), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
+ else
+ displayText = Loc.GetString("lathe-menu-item-single", ("index", idx + 1), ("name", itemName));
- queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe));
+ if (idx >= oldChildCount)
+ {
+ var queuedRecipeBox = new QueuedRecipeControl(displayText, idx, GetRecipeDisplayControl(recipe));
+ queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
+ queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
+ queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
+ QueueList.AddChild(queuedRecipeBox);
+ }
+ else
+ {
+ var child = QueueList.GetChild(idx) as QueuedRecipeControl;
- var queuedRecipeLabel = new Label();
- queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}";
- queuedRecipeBox.AddChild(queuedRecipeLabel);
- QueueList.AddChild(queuedRecipeBox);
+ if (child == null)
+ {
+ DebugTools.Assert($"Lathe menu queued recipe control at {idx} is not of type QueuedRecipeControl"); // Something's gone terribly wrong.
+ continue;
+ }
+
+ child.SetDisplayText(displayText);
+ child.SetIndex(idx);
+ child.SetDisplayControl(GetRecipeDisplayControl(recipe));
+ }
idx++;
}
+
+ // Shrink list if new list is shorter than old list.
+ for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
+ {
+ QueueList.RemoveChild(childIdx);
+ }
}
public void SetQueueInfo(ProtoId? recipeProto)
diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
new file mode 100644
index 000000000000..b1d4b496a14d
--- /dev/null
+++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+ Caution
+ OpenLeft
+
+
+
+
diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
new file mode 100644
index 000000000000..69c8da6d7b71
--- /dev/null
+++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
@@ -0,0 +1,56 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Lathe.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class QueuedRecipeControl : Control
+{
+ public Action? OnDeletePressed;
+ public Action? OnMoveUpPressed;
+ public Action? OnMoveDownPressed;
+
+ private int _index;
+
+ public QueuedRecipeControl(string displayText, int index, Control displayControl)
+ {
+ RobustXamlLoader.Load(this);
+
+ SetDisplayText(displayText);
+ SetDisplayControl(displayControl);
+ SetIndex(index);
+ _index = index;
+
+ MoveUp.OnPressed += (_) =>
+ {
+ OnMoveUpPressed?.Invoke(_index);
+ };
+
+ MoveDown.OnPressed += (_) =>
+ {
+ OnMoveDownPressed?.Invoke(_index);
+ };
+
+ Delete.OnPressed += (_) =>
+ {
+ OnDeletePressed?.Invoke(_index);
+ };
+ }
+
+ public void SetDisplayText(string displayText)
+ {
+ RecipeName.Text = displayText;
+ }
+
+ public void SetDisplayControl(Control displayControl)
+ {
+ RecipeDisplayContainer.Children.Clear();
+ RecipeDisplayContainer.AddChild(displayControl);
+ }
+
+ public void SetIndex(int index)
+ {
+ _index = index;
+ }
+}
diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml.cs b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
index 4f438c8a8e54..277fe12c046f 100644
--- a/Content.Client/Lathe/UI/RecipeControl.xaml.cs
+++ b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
@@ -2,6 +2,7 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -11,20 +12,47 @@ public sealed partial class RecipeControl : Control
public Action? OnButtonPressed;
public Func TooltipTextSupplier;
+ private ProtoId _recipeId;
+ private LatheSystem _latheSystem;
+
public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, Control displayControl)
{
RobustXamlLoader.Load(this);
- RecipeName.Text = latheSystem.GetRecipeName(recipe);
- RecipeDisplayContainer.AddChild(displayControl);
- Button.Disabled = !canProduce;
+ _latheSystem = latheSystem;
+ _recipeId = recipe.ID;
TooltipTextSupplier = tooltipTextSupplier;
- Button.TooltipSupplier = SupplyTooltip;
+ SetRecipe(recipe);
+ SetCanProduce(canProduce);
+ SetDisplayControl(displayControl);
Button.OnPressed += (_) =>
{
- OnButtonPressed?.Invoke(recipe.ID);
+ OnButtonPressed?.Invoke(_recipeId);
};
+ Button.TooltipSupplier = SupplyTooltip;
+ }
+
+ public void SetRecipe(LatheRecipePrototype recipe)
+ {
+ RecipeName.Text = _latheSystem.GetRecipeName(recipe);
+ _recipeId = recipe.ID;
+ }
+
+ public void SetTooltipSupplier(Func tooltipTextSupplier)
+ {
+ TooltipTextSupplier = tooltipTextSupplier;
+ }
+
+ public void SetCanProduce(bool canProduce)
+ {
+ Button.Disabled = !canProduce;
+ }
+
+ public void SetDisplayControl(Control displayControl)
+ {
+ RecipeDisplayContainer.Children.Clear();
+ RecipeDisplayContainer.AddChild(displayControl);
}
private Control? SupplyTooltip(Control sender)
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index c9b717297bd9..d6eb98e3825c 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Inventory;
@@ -5,6 +6,9 @@
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Station;
using Content.Shared.CCVar;
+using Content.Shared.Clothing;
+using Content.Shared.GameTicking;
+using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
@@ -18,6 +22,7 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
+using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -75,20 +80,11 @@ public override void Initialize()
});
//begin starlight
- _configurationManager.OnValueChanged(StarlightCCVars.ICSecrets, args =>
- {
- _profileEditor?.RefreshCharacterInfo();
- });
-
- _configurationManager.OnValueChanged(StarlightCCVars.OOCNotes, args =>
- {
- _profileEditor?.RefreshCharacterInfo();
- });
-
- _configurationManager.OnValueChanged(StarlightCCVars.ExploitableSecrets, args =>
- {
- _profileEditor?.RefreshCharacterInfo();
- });
+ _configurationManager.OnValueChanged(StarlightCCVars.ICSecrets, _ => _profileEditor?.RefreshCharacterInfo());
+ _configurationManager.OnValueChanged(StarlightCCVars.OOCNotes, _ => _profileEditor?.RefreshCharacterInfo());
+ _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshEditors());
+ _configurationManager.OnValueChanged(CCVars.GameRoleLoadoutTimers, _ => RefreshEditors());
+ _configurationManager.OnValueChanged(StarlightCCVars.ExploitableSecrets, _ => _profileEditor?.RefreshCharacterInfo());
//end starlight
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshEditors());
@@ -377,4 +373,176 @@ private void OpenSavePanel(Action saveAction)
return (_characterSetup, _profileEditor);
}
-}
\ No newline at end of file
+
+ #region Helpers
+
+ ///
+ /// Applies the highest priority job's clothes to the dummy.
+ ///
+ public void GiveDummyJobClothesLoadout(EntityUid dummy, JobPrototype? jobProto, HumanoidCharacterProfile profile)
+ {
+ var job = jobProto ?? GetPreferredJob(profile);
+ GiveDummyJobClothes(dummy, profile, job);
+
+ if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID)))
+ {
+ var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, profile.Species, EntityManager, _prototypeManager);
+ GiveDummyLoadout(dummy, loadout);
+ }
+ }
+
+ ///
+ /// Gets the highest priority job for the profile.
+ ///
+ public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
+ {
+ var highPriorityJob = profile.JobPreferences.FirstOrNull() ?? SharedGameTicker.FallbackOverflowJob;
+ // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
+ return _prototypeManager.Index(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob);
+ }
+
+ public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)
+ {
+ if (roleLoadout == null)
+ return;
+
+ foreach (var group in roleLoadout.SelectedLoadouts.Values)
+ {
+ foreach (var loadout in group)
+ {
+ if (!_prototypeManager.Resolve(loadout.Prototype, out var loadoutProto))
+ continue;
+
+ _spawn.EquipStartingGear(uid, loadoutProto);
+ }
+ }
+ }
+
+ ///
+ /// Applies the specified job's clothes to the dummy.
+ ///
+ public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job)
+ {
+ if (!_inventory.TryGetSlots(dummy, out var slots))
+ return;
+
+ // Apply loadout
+ if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout))
+ {
+ foreach (var loadouts in jobLoadout.SelectedLoadouts.Values)
+ {
+ foreach (var loadout in loadouts)
+ {
+ if (!_prototypeManager.Resolve(loadout.Prototype, out var loadoutProto))
+ continue;
+
+ // TODO: Need some way to apply starting gear to an entity and replace existing stuff coz holy fucking shit dude.
+ foreach (var slot in slots)
+ {
+ // Try startinggear first
+ if (_prototypeManager.Resolve(loadoutProto.StartingGear, out var loadoutGear))
+ {
+ var itemType = ((IEquipmentLoadout) loadoutGear).GetGear(slot.Name);
+
+ if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
+ {
+ EntityManager.DeleteEntity(unequippedItem.Value);
+ }
+
+ if (itemType != string.Empty)
+ {
+ var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
+ _inventory.TryEquip(dummy, item, slot.Name, true, true);
+ }
+ }
+ else
+ {
+ var itemType = ((IEquipmentLoadout) loadoutProto).GetGear(slot.Name);
+
+ if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
+ {
+ EntityManager.DeleteEntity(unequippedItem.Value);
+ }
+
+ if (itemType != string.Empty)
+ {
+ var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
+ _inventory.TryEquip(dummy, item, slot.Name, true, true);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!_prototypeManager.Resolve(job.StartingGear, out var gear))
+ return;
+
+ foreach (var slot in slots)
+ {
+ var itemType = ((IEquipmentLoadout) gear).GetGear(slot.Name);
+
+ if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
+ {
+ EntityManager.DeleteEntity(unequippedItem.Value);
+ }
+
+ if (itemType != string.Empty)
+ {
+ var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
+ _inventory.TryEquip(dummy, item, slot.Name, true, true);
+ }
+ }
+ }
+
+ ///
+ /// Loads the profile onto a dummy entity.
+ ///
+ public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobPrototype? job, bool jobClothes)
+ {
+ EntityUid dummyEnt;
+
+ EntProtoId? previewEntity = null;
+ if (humanoid != null && jobClothes)
+ {
+ job ??= GetPreferredJob(humanoid);
+
+ previewEntity = job.JobPreviewEntity ?? (EntProtoId?)job?.JobEntity;
+ }
+
+ if (previewEntity != null)
+ {
+ // Special type like borg or AI, do not spawn a human just spawn the entity.
+ dummyEnt = EntityManager.SpawnEntity(previewEntity, MapCoordinates.Nullspace);
+ return dummyEnt;
+ }
+ else if (humanoid is not null)
+ {
+ var dummy = _prototypeManager.Index(humanoid.Species).DollPrototype;
+ dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
+ }
+ else
+ {
+ dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
+ }
+
+ _humanoid.LoadProfile(dummyEnt, humanoid);
+
+ if (humanoid != null && jobClothes)
+ {
+ DebugTools.Assert(job != null);
+
+ GiveDummyJobClothes(dummyEnt, humanoid, job);
+
+ if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID)))
+ {
+ var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, humanoid.Species, EntityManager, _prototypeManager);
+ GiveDummyLoadout(dummyEnt, loadout);
+ }
+ }
+
+ return dummyEnt;
+ }
+
+ #endregion
+}
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index 6acba4fe61ac..0c4d1c0dc557 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -984,7 +984,7 @@ private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
if (_prototypeManager.HasIndex(species))
page = new ProtoId(species.Id); // Gross. See above todo comment.
- if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
+ if (_prototypeManager.Resolve(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary, GuideEntry>();
dict.Add(DefaultSpeciesGuidebook, guideRoot);
@@ -1291,63 +1291,40 @@ private void OnSkinColorOnValueChanged()
if (Profile is null) return;
var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
+ var strategy = _prototypeManager.Index(skin).Strategy;
- switch (skin)
+ switch (strategy.InputType)
{
- case HumanoidSkinColor.HumanToned:
+ case SkinColorationStrategyInput.Unary:
+ {
+ if (!Skin.Visible)
{
- if (!Skin.Visible)
- {
- Skin.Visible = true;
- RgbSkinColorContainer.Visible = false;
- }
-
- var color = SkinColor.HumanSkinTone((int)Skin.Value);
-
- Markings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
- break;
+ Skin.Visible = true;
+ RgbSkinColorContainer.Visible = false;
}
- case HumanoidSkinColor.Hues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
- Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
- break;
- }
- case HumanoidSkinColor.TintedHues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
+ var color = strategy.FromUnary(Skin.Value);
- var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
+ Markings.CurrentSkinColor = color;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
- Markings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
- break;
- }
- case HumanoidSkinColor.VoxFeathers:
+ break;
+ }
+ case SkinColorationStrategyInput.Color:
+ {
+ if (!RgbSkinColorContainer.Visible)
{
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
- var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
+ var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color);
- Markings.CurrentSkinColor = color;
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
- break;
- }
+ Markings.CurrentSkinColor = color;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
+
+ break;
+ }
}
ReloadProfilePreview();
@@ -1539,7 +1516,7 @@ private void UpdateSexControls()
var sexes = new List();
// add species sex options, default to just none if we are in bizzaro world and have no species
- if (_prototypeManager.TryIndex(Profile.Species, out var speciesProto))
+ if (_prototypeManager.Resolve(Profile.Species, out var speciesProto))
{
foreach (var sex in speciesProto.Sexes)
{
@@ -1589,57 +1566,34 @@ private void UpdateSkinColor()
return;
var skin = _prototypeManager.Index(Profile.Species).SkinColoration;
+ var strategy = _prototypeManager.Index(skin).Strategy;
- switch (skin)
+ switch (strategy.InputType)
{
- case HumanoidSkinColor.HumanToned:
+ case SkinColorationStrategyInput.Unary:
+ {
+ if (!Skin.Visible)
{
- if (!Skin.Visible)
- {
- Skin.Visible = true;
- RgbSkinColorContainer.Visible = false;
- }
-
- Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
-
- break;
+ Skin.Visible = true;
+ RgbSkinColorContainer.Visible = false;
}
- case HumanoidSkinColor.Hues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
- // set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- case HumanoidSkinColor.TintedHues:
- {
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
+ Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor);
- // set the RGB values to the direct values otherwise
- _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
- break;
- }
- case HumanoidSkinColor.VoxFeathers:
+ break;
+ }
+ case SkinColorationStrategyInput.Color:
+ {
+ if (!RgbSkinColorContainer.Visible)
{
- if (!RgbSkinColorContainer.Visible)
- {
- Skin.Visible = false;
- RgbSkinColorContainer.Visible = true;
- }
+ Skin.Visible = false;
+ RgbSkinColorContainer.Visible = true;
+ }
- _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
+ _rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor);
- break;
- }
+ break;
+ }
}
}
@@ -1651,7 +1605,7 @@ public void UpdateSpeciesGuidebookIcon()
if (species is null)
return;
- if (!_prototypeManager.TryIndex(species, out var speciesProto))
+ if (!_prototypeManager.Resolve(species, out var speciesProto))
return;
// Don't display the info button if no guide entry is found
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs
index 2264cecd230e..035f4a3c1a82 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs
@@ -40,7 +40,7 @@ public LoadoutContainer(ProtoId proto, bool disabled, Formatte
SelectButton.TooltipSupplier = _ => tooltip;
}
- if (_protoManager.TryIndex(proto, out var loadProto))
+ if (_protoManager.Resolve(proto, out var loadProto))
{
var ent = loadProto.DummyEntity ?? _entManager.System().GetFirstOrNull(loadProto);
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
index b06e7e41d1fc..644910db601c 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
@@ -62,7 +62,7 @@ public void RefreshLoadouts(HumanoidCharacterProfile profile, RoleLoadout loadou
});
}
- if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
+ if (protoMan.Resolve(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
{
RestrictionsContainer.AddChild(new Label()
{
@@ -112,14 +112,14 @@ public void RefreshLoadouts(HumanoidCharacterProfile profile, RoleLoadout loadou
})
.ToList();
- /*
- * Determine which element should be displayed first:
- * - If any element is currently selected (its button is pressed), use it.
- * - Otherwise, fallback to the first element in the list.
- *
- * This moves the selected item outside of the sublist for better usability,
- * making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
- * without having to search inside expanded subgroups.
+ /*
+ * Determine which element should be displayed first:
+ * - If any element is currently selected (its button is pressed), use it.
+ * - Otherwise, fallback to the first element in the list.
+ *
+ * This moves the selected item outside of the sublist for better usability,
+ * making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
+ * without having to search inside expanded subgroups.
*/
var firstElement = uiElements.FirstOrDefault(e => e.Select.Pressed) ?? uiElements[0];
@@ -195,8 +195,8 @@ private void UpdateSubGroupSelectedInfo(LoadoutContainer loadout, string itemNam
///
/// Creates a UI container for a single Loadout item.
///
- /// This method was extracted from RefreshLoadouts because the logic for creating
- /// individual loadout items is used multiple times inside that method, and duplicating
+ /// This method was extracted from RefreshLoadouts because the logic for creating
+ /// individual loadout items is used multiple times inside that method, and duplicating
/// the code made it harder to maintain.
///
/// Logic:
diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
index 68e1ecbeaeef..50860b349a9e 100644
--- a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
+++ b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
@@ -68,7 +68,7 @@ public LoadoutWindow(HumanoidCharacterProfile profile, RoleLoadout loadout, Role
{
foreach (var group in proto.Groups)
{
- if (!protoManager.TryIndex(group, out var groupProto))
+ if (!protoManager.Resolve(group, out var groupProto))
continue;
if (groupProto.Hidden)
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
index 340cc9af891c..651c76e61f17 100644
--- a/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringNavMapControl.cs
@@ -64,7 +64,9 @@ protected override void FrameUpdate(FrameEventArgs args)
if (!LocalizedNames.TryGetValue(netEntity, out var name))
name = "Unknown";
- var message = name + "\nLocation: [x = " + MathF.Round(blip.Coordinates.X) + ", y = " + MathF.Round(blip.Coordinates.Y) + "]";
+ var message = name + "\n" + Loc.GetString("navmap-location",
+ ("x", MathF.Round(blip.Coordinates.X)),
+ ("y", MathF.Round(blip.Coordinates.Y)));
_trackedEntityLabel.Text = message;
_trackedEntityPanel.Visible = true;
diff --git a/Content.Client/Morgue/CrematoriumSystem.cs b/Content.Client/Morgue/CrematoriumSystem.cs
new file mode 100644
index 000000000000..66eac263c25a
--- /dev/null
+++ b/Content.Client/Morgue/CrematoriumSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Morgue;
+
+namespace Content.Client.Morgue;
+
+public sealed class CrematoriumSystem : SharedCrematoriumSystem;
diff --git a/Content.Client/Morgue/MorgueSystem.cs b/Content.Client/Morgue/MorgueSystem.cs
new file mode 100644
index 000000000000..b8d2f109fbe3
--- /dev/null
+++ b/Content.Client/Morgue/MorgueSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Morgue;
+
+namespace Content.Client.Morgue;
+
+public sealed class MorgueSystem : SharedMorgueSystem;
diff --git a/Content.Client/Movement/Systems/ContentEyeSystem.cs b/Content.Client/Movement/Systems/ContentEyeSystem.cs
index 518a4a1bd40a..a332d25f9a16 100644
--- a/Content.Client/Movement/Systems/ContentEyeSystem.cs
+++ b/Content.Client/Movement/Systems/ContentEyeSystem.cs
@@ -1,7 +1,6 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
-using Robust.Client.GameObjects;
using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
@@ -63,4 +62,15 @@ public override void FrameUpdate(float frameTime)
UpdateEyeOffset((entity, eyeComponent));
}
}
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ // TODO: Ideally we wouldn't want this to run in both FrameUpdate and Update, but we kind of have to since the visual update happens in FrameUpdate, but interaction update happens in Update. It's a workaround and a better solution should be found.
+ var eyeEntities = AllEntityQuery();
+ while (eyeEntities.MoveNext(out var entity, out ContentEyeComponent? contentComponent, out EyeComponent? eyeComponent))
+ {
+ UpdateEyeOffset((entity, eyeComponent));
+ }
+ }
}
diff --git a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
index eb524cf4ee2d..174ae2dd970c 100644
--- a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
+++ b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
@@ -1,10 +1,10 @@
using System.Numerics;
using Content.Client.Movement.Components;
+using Content.Client.Viewport;
using Content.Shared.Camera;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Map;
-using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
@@ -12,13 +12,10 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
- [Dependency] private readonly IPlayerManager _player = default!;
- [Dependency] private readonly SharedTransformSystem _transform = default!;
- [Dependency] private readonly IClyde _clyde = default!;
// This value is here to make sure the user doesn't have to move their mouse
// all the way out to the edge of the screen to get the full offset.
- static private float _edgeOffset = 0.9f;
+ private static float _edgeOffset = 0.8f;
public override void Initialize()
{
@@ -38,25 +35,29 @@ private void OnGetEyeOffsetEvent(EntityUid uid, EyeCursorOffsetComponent compone
public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
{
- var localPlayer = _player.LocalEntity;
- var mousePos = _inputManager.MouseScreenPosition;
- var screenSize = _clyde.MainWindow.Size;
- var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;
+ // We need the main viewport where the game content is displayed, as certain UI layouts (e.g. Separated HUD) can make it a different size to the game window.
+ if (_eyeManager.MainViewport is not ScalingViewport vp)
+ return null;
- var mouseNormalizedPos = new Vector2(-(mousePos.X - screenSize.X / 2) / minValue, (mousePos.Y - screenSize.Y / 2) / minValue); // X needs to be inverted here for some reason, otherwise it ends up flipped.
+ var mousePos = _inputManager.MouseScreenPosition.Position; // TODO: If we ever get a right-aligned Separated HUD setting, this might need to be adjusted for that.
- if (localPlayer == null)
- return null;
+ var viewportSize = vp.PixelSize; // The size of the game viewport, including black bars - does not include the chatbox in Separated HUD view.
+ var scalingViewportSize = vp.ViewportSize * vp.CurrentRenderScale; // The size of the viewport in which the game is rendered (i.e. not including black bars). Note! Can extend outside the game window with certain zoom settings!
+ var visibleViewportSize = Vector2.Min(viewportSize, scalingViewportSize); // The size of the game viewport that is "actually visible" to the player, cutting off over-extensions and not counting black bar padding.
+
+ Matrix3x2.Invert(_eyeManager.MainViewport.GetLocalToScreenMatrix(), out var matrix);
+ var mouseCoords = Vector2.Transform(mousePos, matrix); // Gives the mouse position inside of the *scaling viewport*, i.e. 0,0 is inside the black bars. Note! 0,0 can be outside the game window with certain zoom settings!
- var playerPos = _transform.GetWorldPosition(localPlayer.Value);
+ var boundedMousePos = Vector2.Clamp(Vector2.Min(mouseCoords, mousePos), Vector2.Zero, visibleViewportSize); // Mouse position inside the visible game viewport's bounds.
+
+ var offsetRadius = MathF.Min(visibleViewportSize.X / 2f, visibleViewportSize.Y / 2f) * _edgeOffset;
+ var mouseNormalizedPos = new Vector2(-(boundedMousePos.X - visibleViewportSize.X / 2f) / offsetRadius, (boundedMousePos.Y - visibleViewportSize.Y / 2f) / offsetRadius);
if (component == null)
- {
component = EnsureComp(uid);
- }
// Doesn't move the offset if the mouse has left the game window!
- if (mousePos.Window != WindowId.Invalid)
+ if (_inputManager.MouseScreenPosition.Window != WindowId.Invalid)
{
// The offset must account for the in-world rotation.
var eyeRotation = _eyeManager.CurrentEye.Rotation;
@@ -77,7 +78,7 @@ private void OnGetEyeOffsetEvent(EntityUid uid, EyeCursorOffsetComponent compone
Vector2 vectorOffset = component.TargetPosition - component.CurrentPosition;
if (vectorOffset.Length() > component.OffsetSpeed)
{
- vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed;
+ vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed; // TODO: Probably needs to properly account for time delta or something.
}
component.CurrentPosition += vectorOffset;
}
diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs
index 0c72a8f99ffd..dc8fd9843312 100644
--- a/Content.Client/NPC/PathfindingSystem.cs
+++ b/Content.Client/NPC/PathfindingSystem.cs
@@ -20,6 +20,7 @@ public sealed class PathfindingSystem : SharedPathfindingSystem
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly NPCSteeringSystem _steering = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
@@ -30,17 +31,15 @@ public PathfindingDebugMode Modes
get => _modes;
set
{
- var overlayManager = IoCManager.Resolve();
-
if (value == PathfindingDebugMode.None)
{
Breadcrumbs.Clear();
Polys.Clear();
- overlayManager.RemoveOverlay();
+ _overlayManager.RemoveOverlay();
}
- else if (!overlayManager.HasOverlay())
+ else if (!_overlayManager.HasOverlay())
{
- overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
+ _overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem));
}
if ((value & PathfindingDebugMode.Steering) != 0x0)
diff --git a/Content.Client/Overlays/EntityHealthBarOverlay.cs b/Content.Client/Overlays/EntityHealthBarOverlay.cs
index 9ff0422aba0f..cf9d879844d8 100644
--- a/Content.Client/Overlays/EntityHealthBarOverlay.cs
+++ b/Content.Client/Overlays/EntityHealthBarOverlay.cs
@@ -57,7 +57,7 @@ protected override void Draw(in OverlayDrawArgs args)
const float scale = 1f;
var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
- _prototype.TryIndex(StatusIcon, out var statusIcon);
+ _prototype.Resolve(StatusIcon, out var statusIcon);
var query = _entManager.AllEntityQueryEnumerator();
while (query.MoveNext(out var uid,
diff --git a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
index c353b1727295..9a84defba026 100644
--- a/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
+++ b/Content.Client/Overlays/ShowCriminalRecordIconsSystem.cs
@@ -22,7 +22,7 @@ private void OnGetStatusIconsEvent(EntityUid uid, CriminalRecordComponent compon
if (!IsActive)
return;
- if (_prototype.TryIndex(component.StatusIcon, out var iconPrototype))
+ if (_prototype.Resolve(component.StatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/ShowHealthIconsSystem.cs b/Content.Client/Overlays/ShowHealthIconsSystem.cs
index 3301261bd09e..0a875e79c582 100644
--- a/Content.Client/Overlays/ShowHealthIconsSystem.cs
+++ b/Content.Client/Overlays/ShowHealthIconsSystem.cs
@@ -78,9 +78,9 @@ private IReadOnlyList DecideHealthIcons(Entity(entity, out var state))
{
// Since there is no MobState for a rotting mob, we have to deal with this case first.
- if (HasComp(entity) && _prototypeMan.TryIndex(damageableComponent.RottingIcon, out var rottingIcon))
+ if (HasComp(entity) && _prototypeMan.Resolve(damageableComponent.RottingIcon, out var rottingIcon))
result.Add(rottingIcon);
- else if (damageableComponent.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.TryIndex(value, out var icon))
+ else if (damageableComponent.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.Resolve(value, out var icon))
result.Add(icon);
}
}
diff --git a/Content.Client/Overlays/ShowJobIconsSystem.cs b/Content.Client/Overlays/ShowJobIconsSystem.cs
index d0d14449f66a..faf4024c2fa3 100644
--- a/Content.Client/Overlays/ShowJobIconsSystem.cs
+++ b/Content.Client/Overlays/ShowJobIconsSystem.cs
@@ -51,7 +51,7 @@ private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref Get
}
}
- if (_prototype.TryIndex(iconId, out var iconPrototype))
+ if (_prototype.Resolve(iconId, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
else
Log.Error($"Invalid job icon prototype: {iconPrototype}");
diff --git a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
index 8f8b8e640775..33cb3ddd9328 100644
--- a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
+++ b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
@@ -23,7 +23,7 @@ private void OnGetStatusIconsEventFake(EntityUid uid, FakeMindShieldComponent co
{
if(!IsActive)
return;
- if (component.IsEnabled && _prototype.TryIndex(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
+ if (component.IsEnabled && _prototype.Resolve(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
ev.StatusIcons.Add(fakeStatusIconPrototype);
}
@@ -32,7 +32,7 @@ private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component,
if (!IsActive)
return;
- if (_prototype.TryIndex(component.MindShieldStatusIcon, out var iconPrototype))
+ if (_prototype.Resolve(component.MindShieldStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs
index 0796be08e168..55cb1811a513 100644
--- a/Content.Client/Overlays/StencilOverlay.cs
+++ b/Content.Client/Overlays/StencilOverlay.cs
@@ -65,7 +65,7 @@ protected override void Draw(in OverlayDrawArgs args)
{
foreach (var (proto, weather) in comp.Weather)
{
- if (!_protoManager.TryIndex(proto, out var weatherProto))
+ if (!_protoManager.Resolve(proto, out var weatherProto))
continue;
var alpha = _weather.GetPercent(weather, mapUid);
diff --git a/Content.Client/Paper/UI/PaperBoundUserInterface.cs b/Content.Client/Paper/UI/PaperBoundUserInterface.cs
index ec417f749b9b..1b385ac810a2 100644
--- a/Content.Client/Paper/UI/PaperBoundUserInterface.cs
+++ b/Content.Client/Paper/UI/PaperBoundUserInterface.cs
@@ -23,6 +23,7 @@ protected override void Open()
_window = this.CreateWindow();
_window.OnSaved += InputOnTextEntered;
+ _window.OnSignatureRequested += OnSignatureRequested; // Starlight-edit
if (EntMan.TryGetComponent(Owner, out var paper))
{
@@ -50,4 +51,6 @@ private void InputOnTextEntered(string text)
_window.Input.CursorPosition = new TextEdit.CursorPos(0, TextEdit.LineBreakBias.Top);
}
}
+
+ private void OnSignatureRequested(int signatureIndex) => SendMessage(new PaperSignatureRequestMessage(signatureIndex)); // Starlight-edit
}
diff --git a/Content.Client/Paper/UI/PaperWindow.xaml b/Content.Client/Paper/UI/PaperWindow.xaml
index 5e882b29e6c3..799cca647f1e 100644
--- a/Content.Client/Paper/UI/PaperWindow.xaml
+++ b/Content.Client/Paper/UI/PaperWindow.xaml
@@ -14,7 +14,11 @@
-
+
+
+
+
+
diff --git a/Content.Client/Paper/UI/PaperWindow.xaml.cs b/Content.Client/Paper/UI/PaperWindow.xaml.cs
index 6971476ab8e6..814922554ffe 100644
--- a/Content.Client/Paper/UI/PaperWindow.xaml.cs
+++ b/Content.Client/Paper/UI/PaperWindow.xaml.cs
@@ -1,28 +1,32 @@
using System.Numerics;
using Content.Client.UserInterface.RichText;
using Content.Shared.Paper;
+using static Content.Shared.Paper.PaperComponent; // Starlight-edit
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Input;
+using Robust.Shared.IoC;
namespace Content.Client.Paper.UI
{
[GenerateTypedNameReferences]
public sealed partial class PaperWindow : BaseWindow
{
+ private PaperComponent.PaperBoundUserInterfaceState _currentState = default!;
+ private string _currentRawText = string.Empty;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
private static Color DefaultTextColor = new(25, 25, 25);
- //
// Size of resize handles around the paper
private const int DRAG_MARGIN_SIZE = 16;
@@ -37,6 +41,9 @@ public sealed partial class PaperWindow : BaseWindow
// If paper limits the size in one or both axes, it'll affect whether
// we're able to resize this UI or not. Default to everything enabled:
private DragMode _allowedResizeModes = ~DragMode.None;
+
+ // Store original margin to restore when switching modes
+ private Thickness _originalContentMargin;
private readonly Type[] _allowedTags = new Type[] {
typeof(BoldItalicTag),
@@ -46,20 +53,21 @@ public sealed partial class PaperWindow : BaseWindow
typeof(HeadingTag),
typeof(ItalicTag),
typeof(MonoTag),
+ typeof(FormTagHandler),
+ typeof(SignatureTagHandler),
typeof(LogoTag),
typeof(SyndieLogoTag),
- typeof(CCLogoTag)
+ typeof(CCLogoTag),
+ typeof(CheckTagHandler)
};
public event Action? OnSaved;
+ public event Action? OnSignatureRequested;
private int _MaxInputLength = -1;
public int MaxInputLength
{
- get
- {
- return _MaxInputLength;
- }
+ get => _MaxInputLength;
set
{
_MaxInputLength = value;
@@ -107,9 +115,11 @@ public PaperWindow()
}
///
- /// Initialize this UI according to visuals Initializes
- /// textures, recalculates sizes, and applies some layout rules.
+ /// Initialize the paper window's visual appearance based on the paper entity's visual component.
+ /// Sets up background textures, header/footer images, content styling, and size constraints.
///
+ /// The paper entity UID used for stamp placement randomization
+ /// Visual configuration containing textures, colors, and layout settings
public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
{
// Randomize the placement of any stamps based on the entity UID
@@ -133,14 +143,12 @@ public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
PatchMarginRight = backgroundPatchMargin.Right,
PatchMarginTop = backgroundPatchMargin.Top
};
-
}
else
{
PaperBackground.PanelOverride = null;
}
-
// Then the header:
if (visuals.HeaderImagePath != null)
{
@@ -164,7 +172,6 @@ public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
visuals.FooterMargin.Right, visuals.FooterMargin.Bottom);
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
- WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
FillStatus.ModulateSelfOverride = visuals.FontAccentColor;
var contentImage = visuals.ContentImagePath != null ? _resCache.GetResource(visuals.ContentImagePath) : null;
@@ -181,9 +188,10 @@ public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
_paperContentLineScale = visuals.ContentImageNumLines;
}
- PaperContent.Margin = new Thickness(
+ _originalContentMargin = new Thickness(
visuals.ContentMargin.Left, visuals.ContentMargin.Top,
visuals.ContentMargin.Right, visuals.ContentMargin.Bottom);
+ PaperContent.Margin = _originalContentMargin;
if (visuals.MaxWritableArea != null)
{
@@ -216,149 +224,539 @@ public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
}
///
- /// Control interface. We'll mostly rely on the children to do the drawing
- /// but in order to get lines on paper to match up with the rich text labels,
- /// we need to do a small calculation to sync them up.
+ /// Custom drawing logic to align text with paper line textures.
+ /// Calculates font metrics and adjusts texture scaling so that text baselines
+ /// align perfectly with the paper's ruled lines.
///
+ /// Drawing handle for rendering operations
protected override void Draw(DrawingHandleScreen handle)
{
- // Now do the deferred setup of the written area. At the point
- // that InitVisuals runs, the label hasn't had it's style initialized
- // so we need to get some info out now:
+ // Deferred setup of the written area - we need font metrics that aren't available during InitVisuals
+ // The label's style must be initialized before we can get font information
if (WrittenTextLabel.TryGetStyleProperty("font", out var font))
{
float fontLineHeight = font.GetLineHeight(1.0f);
- // This positions the texture so the font baseline is on the bottom:
+
+ // Set the font line height in tag handlers so buttons match text height
+ FormTagHandler.FontLineHeight = fontLineHeight;
+ SignatureTagHandler.FontLineHeight = fontLineHeight;
+ CheckTagHandler.FontLineHeight = fontLineHeight;
+
+ // Position the background texture so font baseline aligns with texture lines
_paperContentTex.ExpandMarginTop = font.GetDescent(UIScale);
- // And this scales the texture so that it's a single text line:
+
+ // Scale the texture vertically so it represents the correct number of text lines
var scaleY = (_paperContentLineScale * fontLineHeight) / _paperContentTex.Texture?.Height ?? fontLineHeight;
_paperContentTex.TextureScale = new Vector2(1, scaleY);
- // Now, we might need to add some padding to the text to ensure
- // that, even if a header is specified, the text will line up with
- // where the content image expects the font to be rendered (i.e.,
- // adjusting the height of the header image shouldn't cause the
- // text to be offset from a line)
- {
- var headerHeight = HeaderImage.Size.Y + HeaderImage.Margin.Top + HeaderImage.Margin.Bottom;
- var headerInLines = headerHeight / (fontLineHeight * _paperContentLineScale);
- var paddingRequiredInLines = (float)Math.Ceiling(headerInLines) - headerInLines;
- var verticalMargin = fontLineHeight * paddingRequiredInLines * _paperContentLineScale;
- TextAlignmentPadding.Margin = new Thickness(0.0f, verticalMargin, 0.0f, 0.0f);
- }
+ // Calculate padding to ensure text aligns with paper lines even with headers
+ // This prevents header images from throwing off the text-to-line alignment
+ var headerHeight = HeaderImage.Size.Y + HeaderImage.Margin.Top + HeaderImage.Margin.Bottom;
+ var headerInLines = headerHeight / (fontLineHeight * _paperContentLineScale);
+ var paddingRequiredInLines = (float)Math.Ceiling(headerInLines) - headerInLines;
+ var verticalMargin = fontLineHeight * paddingRequiredInLines * _paperContentLineScale;
+ TextAlignmentPadding.Margin = new Thickness(0.0f, verticalMargin, 0.0f, 0.0f);
}
base.Draw(handle);
}
///
- /// Initialize the paper contents, i.e. the text typed by the
- /// user and any stamps that have peen put on the page.
+ /// Populate the paper window with content based on the current paper state.
+ /// Handles both editing mode (showing text input) and reading mode (showing formatted text).
+ /// Processes markup tags like [form] and [signature] for interactive elements.
///
+ /// Current paper state containing text, mode, and stamp information
public void Populate(PaperComponent.PaperBoundUserInterfaceState state)
{
- bool isEditing = state.Mode == PaperComponent.PaperAction.Write;
- bool wasEditing = InputContainer.Visible;
+ _currentState = state;
+ _currentRawText = state.Text;
+ var isEditing = state.Mode == PaperComponent.PaperAction.Write;
+
+ // Show/hide UI elements based on edit mode
InputContainer.Visible = isEditing;
EditButtons.Visible = isEditing;
+ WrittenTextLabel.Visible = !isEditing;
+ WrittenTextContainer.Visible = false;
+ BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
- var msg = new FormattedMessage();
- msg.AddMarkupPermissive(state.Text);
-
- // For premade documents, we want to be able to edit them rather than
- // replace them.
- var shouldCopyText = 0 == Input.TextLength && 0 != state.Text.Length;
- if (!wasEditing || shouldCopyText)
- {
- // We can get repeated messages with state.Mode == Write if another
- // player opens the UI for reading. In this case, don't update the
- // text input, as this player is currently writing new text and we
- // don't want to lose any text they already input.
- Input.TextRope = Rope.Leaf.Empty;
- Input.CursorPosition = new TextEdit.CursorPos();
- Input.InsertAtCursor(state.Text);
- }
-
- for (var i = 0; i <= state.StampedBy.Count * 3 + 1; i++)
+ if (isEditing)
{
- msg.AddMarkupPermissive("\r\n");
+ // Reset margin to original when editing (no tag buttons visible)
+ PaperContent.Margin = _originalContentMargin;
+
+ // Initialize the text input field with server content if it's currently empty
+ // This allows editing existing documents while preserving any text the user has already typed
+ var shouldCopy = Input.TextLength == 0 && state.Text.Length > 0;
+ if (shouldCopy)
+ {
+ // We can get repeated messages with state.Mode == Write if another
+ // player opens the UI for reading. In this case, don't update the
+ // text input, as this player is currently writing new text and we
+ // don't want to lose any text they already input.
+ Input.TextRope = Rope.Leaf.Empty;
+ Input.CursorPosition = new TextEdit.CursorPos();
+ Input.InsertAtCursor(state.Text);
+ }
+ return;
}
- WrittenTextLabel.RemoveAllChildren();
- WrittenTextLabel.SetMessage(msg, _allowedTags, DefaultTextColor);
-
- WrittenTextLabel.Visible = !isEditing && state.Text.Length > 0;
- BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
-
+ // Reset form, signature, and check counters before processing to ensure consistent indexing
+ // This is crucial because the tag handlers maintain state between renders
+ FormTagHandler.SetFormText(state.Text);
+ FormTagHandler.ResetFormCounter();
+ SignatureTagHandler.ResetSignatureCounter();
+ CheckTagHandler.ResetCheckCounter();
+
+ // Display text with markup processing (forms, signatures, colors, etc.)
+ // The markup system converts [form] and [signature] tags into interactive buttons
+ var fm = new FormattedMessage();
+ fm.AddMarkupPermissive(state.Text);
+ WrittenTextLabel.SetMessage(fm, _allowedTags, DefaultTextColor);
+
+ // Add extra bottom margin based on tag count to prevent cutoff (only in read mode)
+ var tagCount = CountTags(state.Text);
+ var extraBottomMargin = tagCount * 3.0f; // 3 pixels per tag for extra height
+ PaperContent.Margin = new Thickness(_originalContentMargin.Left, _originalContentMargin.Top,
+ _originalContentMargin.Right, _originalContentMargin.Bottom + extraBottomMargin);
+
+ // Add stamps that have been applied to this paper
+ // Clear existing stamps first, then add all current ones
StampDisplay.RemoveAllChildren();
StampDisplay.RemoveStamps();
- foreach(var stamper in state.StampedBy)
- {
- StampDisplay.AddStamp(new StampWidget{ StampInfo = stamper });
- }
+ foreach (var stamper in state.StampedBy)
+ StampDisplay.AddStamp(new StampWidget { StampInfo = stamper });
}
///
- /// BaseWindow interface. Allow users to drag UI around by grabbing
- /// anywhere on the page (like FancyWindow) but try to calculate
- /// reasonable dragging bounds because this UI can have round corners,
- /// and it can be hard to judge where to click to resize.
+ /// Base Window. Determines how the window can be dragged/resized based on mouse position (Like FancyWindow).
+ /// Provides generous resize margins since paper UI may have rounded corners
+ /// that make precise edge clicking difficult.
///
+ /// Mouse position relative to window
+ /// Drag mode indicating which edges can be used for resizing
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
var mode = DragMode.None;
// Be quite generous with resize margins:
if (relativeMousePos.Y < DRAG_MARGIN_SIZE)
- {
mode |= DragMode.Top;
- }
else if (relativeMousePos.Y > Size.Y - DRAG_MARGIN_SIZE)
- {
mode |= DragMode.Bottom;
- }
-
if (relativeMousePos.X < DRAG_MARGIN_SIZE)
- {
mode |= DragMode.Left;
- }
else if (relativeMousePos.X > Size.X - DRAG_MARGIN_SIZE)
- {
mode |= DragMode.Right;
- }
- if((mode & _allowedResizeModes) == DragMode.None)
- {
+ if ((mode & _allowedResizeModes) == DragMode.None)
return DragMode.Move;
- }
+
return mode & _allowedResizeModes;
}
+ ///
+ /// Handles the save operation by invoking the OnSaved event with current text.
+ /// Temporarily disables the save button to prevent multiple simultaneous saves.
+ ///
private void RunOnSaved()
{
- // Prevent further saving while text processing still in
+ // Prevent further saving while text processing is in progress
SaveButton.Disabled = true;
OnSaved?.Invoke(Rope.Collapse(Input.TextRope));
+ SaveButton.Disabled = false;
}
+ ///
+ /// Updates the fill status display and save button state based on text length limits.
+ /// Shows current character count vs maximum allowed, disables save if over limit.
+ ///
private void UpdateFillState()
{
if (MaxInputLength != -1)
{
var inputLength = Input.TextLength;
-
+ // Display current vs maximum character count
FillStatus.Text = Loc.GetString("paper-ui-fill-level",
("currentLength", inputLength),
("maxLength", MaxInputLength));
-
- // Disable the save button if we've gone over the limit
+ // Disable the save button if we've exceeded the character limit
SaveButton.Disabled = inputLength > MaxInputLength;
}
else
{
+ // No limit set, clear status and enable saving
FillStatus.Text = "";
SaveButton.Disabled = false;
}
}
+
+ ///
+ /// Removes any unfilled [form] and [signature] tags from the paper text.
+ /// Called when the paper is stamped to finalize the document.
+ ///
+ /// The paper text to clean
+ /// Text with unfilled tags removed
+ public static string CleanUnfilledTags(string text)
+ {
+ return text.Replace("[form]", string.Empty).Replace("[signature]", string.Empty);
+ }
+
+ ///
+ /// Opens a modal dialog allowing the user to fill in a specific [form] tag.
+ /// Creates a popup with text input, OK/Cancel buttons, and handles form submission.
+ ///
+ /// Zero-based index of which [form] tag to replace
+ public void OpenFormDialog(int formIndex)
+ {
+ // Find and highlight the form button
+ var formButton = FindFormButton(formIndex);
+ if (formButton != null)
+ formButton.ModulateSelfOverride = Color.LightBlue;
+
+ // Create the popup dialog structure
+ var popup = new Popup();
+ var vbox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Vertical, Margin = new Thickness(10) };
+ var editContainer = new PanelContainer { StyleClasses = { "TransparentBorderedWindowPanel" } };
+ var edit = new LineEdit { MinSize = new Vector2(200, 0), Margin = new Thickness(5) };
+ var hbox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
+ var ok = new Button { Text = Loc.GetString("paper-form-dialog-ok") };
+ var cancel = new Button { Text = Loc.GetString("paper-form-dialog-cancel") };
+
+ editContainer.AddChild(edit);
+
+ // Handle OK button press - save the form data if text was entered
+ ok.OnPressed += _ =>
+ {
+ if (!string.IsNullOrEmpty(edit.Text))
+ {
+ var newText = ReplaceNthFormTag(_currentRawText, formIndex, edit.Text);
+ OnSaved?.Invoke(newText);
+ }
+ if (formButton != null)
+ formButton.ModulateSelfOverride = null;
+ popup.Close();
+ };
+
+ // Handle Cancel button - just close without saving
+ cancel.OnPressed += _ => {
+ if (formButton != null)
+ formButton.ModulateSelfOverride = null;
+ popup.Close();
+ };
+
+ // Handle Enter key in text field - same as OK button
+ edit.OnTextEntered += _ =>
+ {
+ if (!string.IsNullOrEmpty(edit.Text))
+ {
+ var newText = ReplaceNthFormTag(_currentRawText, formIndex, edit.Text);
+ OnSaved?.Invoke(newText);
+ }
+ popup.Close();
+ };
+
+ // Assemble the dialog layout and show it
+ hbox.AddChild(ok);
+ hbox.AddChild(cancel);
+ vbox.AddChild(editContainer);
+ vbox.AddChild(hbox);
+ popup.AddChild(vbox);
+ AddChild(popup);
+ popup.Open();
+ edit.GrabKeyboardFocus(); // Focus the text input for immediate typing
+ }
+
+ ///
+ /// Sends a signature request to the server to handle signature with proper identity system.
+ ///
+ /// Zero-based index of which [signature] tag to replace
+ public void SendSignatureRequest(int signatureIndex)
+ {
+ OnSignatureRequested?.Invoke(signatureIndex);
+ }
+
+ ///
+ /// Finds a form button by index for visual feedback.
+ ///
+ private Button? FindFormButton(int formIndex)
+ {
+ return FindButtonRecursive(WrittenTextLabel, "Fill", formIndex);
+ }
+
+ ///
+ /// Finds a check button by index for visual feedback.
+ ///
+ private Button? FindCheckButton(int checkIndex)
+ {
+ return FindCheckButtonRecursive(WrittenTextLabel, checkIndex);
+ }
+
+ ///
+ /// Finds check buttons (which can have different text: ☐, ✔, ✖).
+ ///
+ private Button? FindCheckButtonRecursive(Control control, int targetIndex)
+ {
+ var currentIndex = 0;
+ return FindCheckButtonRecursiveHelper(control, targetIndex, ref currentIndex);
+ }
+
+ private Button? FindCheckButtonRecursiveHelper(Control control, int targetIndex, ref int currentIndex)
+ {
+ if (control is Button btn && (btn.Text == "☐" || btn.Text == "✔" || btn.Text == "✖"))
+ {
+ if (currentIndex == targetIndex)
+ return btn;
+ currentIndex++;
+ }
+
+ foreach (Control child in control.Children)
+ {
+ var result = FindCheckButtonRecursiveHelper(child, targetIndex, ref currentIndex);
+ if (result != null)
+ return result;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Recursively searches for a button with specific text and index.
+ ///
+ private Button? FindButtonRecursive(Control control, string buttonText, int targetIndex)
+ {
+ var currentIndex = 0;
+ return FindButtonRecursiveHelper(control, buttonText, targetIndex, ref currentIndex);
+ }
+
+ private Button? FindButtonRecursiveHelper(Control control, string buttonText, int targetIndex, ref int currentIndex)
+ {
+ if (control is Button btn && btn.Text == buttonText)
+ {
+ if (currentIndex == targetIndex)
+ return btn;
+ currentIndex++;
+ }
+
+ foreach (Control child in control.Children)
+ {
+ var result = FindButtonRecursiveHelper(child, buttonText, targetIndex, ref currentIndex);
+ if (result != null)
+ return result;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the current text for tag handlers to access.
+ ///
+ /// Current raw text content
+ public string GetCurrentText()
+ {
+ return _currentRawText;
+ }
+
+ ///
+ /// Saves text content for tag handlers to use.
+ ///
+ /// Text to save
+ public void SaveText(string text)
+ {
+ OnSaved?.Invoke(text);
+ }
+
+ private Popup? _activeCheckPopup;
+ private Button? _activeCheckButton;
+
+ ///
+ /// Opens a modal dialog allowing the user to select a check state.
+ ///
+ /// Zero-based index of which [check] tag to replace
+ public void OpenCheckDialog(int checkIndex)
+ {
+ // Close any existing check popup
+ if (_activeCheckPopup != null)
+ {
+ if (_activeCheckButton != null)
+ _activeCheckButton.ModulateSelfOverride = null;
+ _activeCheckPopup.Close();
+ }
+
+ // Find and highlight the check button
+ var checkButton = FindCheckButton(checkIndex);
+ if (checkButton != null)
+ checkButton.ModulateSelfOverride = Color.LightBlue;
+ _activeCheckButton = checkButton;
+
+ var popup = new Popup();
+ _activeCheckPopup = popup;
+ var vbox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Vertical, Margin = new Thickness(10) };
+ var hbox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
+
+ var blankBtn = new Button { Text = "☐ Blank", MinWidth = 80 };
+ var checkBtn = new Button { Text = "✔ Check", MinWidth = 80 };
+ var crossBtn = new Button { Text = "✖ Cross", MinWidth = 80 };
+
+ blankBtn.OnPressed += _ => {
+ var newText = ReplaceNthCheckTag(_currentRawText, checkIndex, "☐");
+ OnSaved?.Invoke(newText);
+ CloseCheckDialog();
+ };
+
+ checkBtn.OnPressed += _ => {
+ var newText = ReplaceNthCheckTag(_currentRawText, checkIndex, "✔");
+ OnSaved?.Invoke(newText);
+ CloseCheckDialog();
+ };
+
+ crossBtn.OnPressed += _ => {
+ var newText = ReplaceNthCheckTag(_currentRawText, checkIndex, "✖");
+ OnSaved?.Invoke(newText);
+ CloseCheckDialog();
+ };
+
+ hbox.AddChild(blankBtn);
+ hbox.AddChild(checkBtn);
+ hbox.AddChild(crossBtn);
+ vbox.AddChild(hbox);
+ popup.AddChild(vbox);
+ AddChild(popup);
+ popup.Open();
+ }
+
+ private void CloseCheckDialog()
+ {
+ if (_activeCheckButton != null)
+ _activeCheckButton.ModulateSelfOverride = null;
+ if (_activeCheckPopup != null)
+ _activeCheckPopup.Close();
+ _activeCheckButton = null;
+ _activeCheckPopup = null;
+ }
+
+ ///
+ /// Replaces the nth occurrence of [check] tag with replacement symbol.
+ ///
+ private static string ReplaceNthCheckTag(string text, int index, string replacement)
+ {
+ const string checkTag = "[check]";
+ var currentIndex = 0;
+ var pos = 0;
+
+ while (pos < text.Length)
+ {
+ var foundPos = text.IndexOf(checkTag, pos);
+ if (foundPos == -1) break;
+
+ if (currentIndex == index)
+ {
+ return text.Substring(0, foundPos) + replacement + text.Substring(foundPos + checkTag.Length);
+ }
+
+ currentIndex++;
+ pos = foundPos + checkTag.Length;
+ }
+
+ return text;
+ }
+
+ ///
+ /// Replaces the nth occurrence of [form] tag with replacement text.
+ /// Uses IndexOf for efficient searching rather than splitting the entire string.
+ ///
+ /// The text containing form tags
+ /// Zero-based index of which form tag to replace
+ /// Text to replace the form tag with
+ /// Text with the specified form tag replaced, or original text if index not found
+ private static string ReplaceNthFormTag(string text, int index, string replacement)
+ {
+ const string formTag = "[form]";
+ var currentIndex = 0;
+ var pos = 0;
+
+ // Search through the text for form tags
+ while (pos < text.Length)
+ {
+ var foundPos = text.IndexOf(formTag, pos);
+ if (foundPos == -1) break; // No more tags found
+
+ // Check if this is the tag we want to replace
+ if (currentIndex == index)
+ {
+ // Replace this specific occurrence: text before + replacement + text after
+ return text.Substring(0, foundPos) + replacement + text.Substring(foundPos + formTag.Length);
+ }
+
+ // Move to the next tag
+ currentIndex++;
+ pos = foundPos + formTag.Length;
+ }
+
+ // Index not found, return original text unchanged
+ return text;
+ }
+
+ ///
+ /// Replaces the nth occurrence of [signature] tag with replacement text.
+ /// Uses IndexOf for efficient searching rather than splitting the entire string.
+ ///
+ /// The text containing signature tags
+ /// Zero-based index of which signature tag to replace
+ /// Text to replace the signature tag with
+ /// Text with the specified signature tag replaced, or original text if index not found
+ private static string ReplaceNthSignatureTag(string text, int index, string replacement)
+ {
+ const string signatureTag = "[signature]";
+ var currentIndex = 0;
+ var pos = 0;
+
+ // Search through the text for signature tags
+ while (pos < text.Length)
+ {
+ var foundPos = text.IndexOf(signatureTag, pos);
+ if (foundPos == -1) break; // No more tags found
+
+ // Check if this is the tag we want to replace
+ if (currentIndex == index)
+ {
+ // Replace this specific occurrence: text before + replacement + text after
+ return text.Substring(0, foundPos) + replacement + text.Substring(foundPos + signatureTag.Length);
+ }
+
+ // Move to the next tag
+ currentIndex++;
+ pos = foundPos + signatureTag.Length;
+ }
+
+ // Index not found, return original text unchanged
+ return text;
+ }
+
+ ///
+ /// Counts the total number of interactive tags that create taller buttons.
+ ///
+ private static int CountTags(string text)
+ {
+ var formCount = CountOccurrences(text, "[form]");
+ var signatureCount = CountOccurrences(text, "[signature]");
+ var checkCount = CountOccurrences(text, "[check]");
+ return formCount + signatureCount + checkCount;
+ }
+
+ ///
+ /// Counts occurrences of a substring in text.
+ ///
+ private static int CountOccurrences(string text, string substring)
+ {
+ var count = 0;
+ var pos = 0;
+ while ((pos = text.IndexOf(substring, pos)) != -1)
+ {
+ count++;
+ pos += substring.Length;
+ }
+ return count;
+ }
}
}
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index 2fe5c18fe099..0f95a817c993 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -120,8 +120,8 @@ public override void SetSprinting(Entity entity, ushort sub
base.SetSprinting(entity, subTick, walking);
if (walking && _cfg.GetCVar(CCVars.ToggleWalk))
- _alerts.ShowAlert(entity, WalkingAlert, showCooldown: false, autoRemove: false);
+ _alerts.ShowAlert(entity.Owner, WalkingAlert, showCooldown: false, autoRemove: false);
else
- _alerts.ClearAlert(entity, WalkingAlert);
+ _alerts.ClearAlert(entity.Owner, WalkingAlert);
}
}
diff --git a/Content.Client/Power/APC/UI/ApcMenu.xaml b/Content.Client/Power/APC/UI/ApcMenu.xaml
index 0ce4a943da7e..6cb46a4360cd 100644
--- a/Content.Client/Power/APC/UI/ApcMenu.xaml
+++ b/Content.Client/Power/APC/UI/ApcMenu.xaml
@@ -20,7 +20,7 @@
-
+
diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs
index c001b7ec70a5..6aa32892cfd8 100644
--- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs
+++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs
@@ -51,10 +51,10 @@ protected override void Open()
_menu.OpenOverMouseScreenPosition();
}
- private IEnumerable ConvertToButtons(HashSet> prototypes)
+ private IEnumerable ConvertToButtons(HashSet> prototypes)
{
- Dictionary> buttonsByCategory = new();
- ValueList topLevelActions = new();
+ Dictionary> buttonsByCategory = new();
+ ValueList topLevelActions = new();
foreach (var protoId in prototypes)
{
var prototype = _prototypeManager.Index(protoId);
@@ -62,7 +62,7 @@ private IEnumerable ConvertToButtons(HashSet(HandleMenuOptionClick, prototype)
{
- Sprite = prototype.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype)
};
topLevelActions.Add(topLevelActionOption);
@@ -74,26 +74,26 @@ private IEnumerable ConvertToButtons(HashSet();
+ list = new List();
buttonsByCategory.Add(prototype.Category, list);
}
var actionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype)
{
- Sprite = prototype.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype)
};
list.Add(actionOption);
}
- var models = new RadialMenuOption[buttonsByCategory.Count + topLevelActions.Count];
+ var models = new RadialMenuOptionBase[buttonsByCategory.Count + topLevelActions.Count];
var i = 0;
foreach (var (key, list) in buttonsByCategory)
{
var groupInfo = PrototypesGroupingInfo[key];
models[i] = new RadialMenuNestedLayerOption(list)
{
- Sprite = groupInfo.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(groupInfo.Sprite),
ToolTip = Loc.GetString(groupInfo.Tooltip)
};
i++;
@@ -125,8 +125,10 @@ private void HandleMenuOptionClick(RCDPrototype proto)
var name = Loc.GetString(proto.SetName);
if (proto.Prototype != null &&
- _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
+ _prototypeManager.Resolve(proto.Prototype, out var entProto))
+ {
name = entProto.Name;
+ }
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
}
@@ -142,7 +144,7 @@ private string GetTooltip(RCDPrototype proto)
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject
&& proto.Prototype != null
- && _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
+ && _prototypeManager.Resolve(proto.Prototype, out var entProto))
{
tooltip = Loc.GetString(entProto.Name);
}
diff --git a/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs b/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
index 784c39a6ced5..1f060e532df2 100644
--- a/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
+++ b/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
@@ -11,6 +11,8 @@ namespace Content.Client.Radiation.Overlays;
public sealed class RadiationDebugOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IResourceCache _cache = default!;
+
private readonly SharedMapSystem _mapSystem;
private readonly RadiationSystem _radiation;
@@ -24,8 +26,7 @@ public RadiationDebugOverlay()
_radiation = _entityManager.System();
_mapSystem = _entityManager.System();
- var cache = IoCManager.Resolve();
- _font = new VectorFont(cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
+ _font = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
}
protected override void Draw(in OverlayDrawArgs args)
diff --git a/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
index 29d6c635ebc8..a2711a8257ca 100644
--- a/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
+++ b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
@@ -1,11 +1,12 @@
using Content.Client.Radio.Ui;
using Content.Shared.Radio;
using Content.Shared.Radio.Components;
+using Content.Shared.Radio.EntitySystems;
using Robust.Client.GameObjects;
namespace Content.Client.Radio.EntitySystems;
-public sealed class RadioDeviceSystem : EntitySystem
+public sealed class RadioDeviceSystem : SharedRadioDeviceSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
diff --git a/Content.Client/Radio/Ui/IntercomMenu.xaml.cs b/Content.Client/Radio/Ui/IntercomMenu.xaml.cs
index f66b3db000e5..887c6f64432b 100644
--- a/Content.Client/Radio/Ui/IntercomMenu.xaml.cs
+++ b/Content.Client/Radio/Ui/IntercomMenu.xaml.cs
@@ -42,7 +42,7 @@ public void Update(Entity entity)
for (var i = 0; i < entity.Comp.SupportedChannels.Count; i++)
{
var channel = entity.Comp.SupportedChannels[i];
- if (!_prototype.TryIndex(channel, out var prototype))
+ if (!_prototype.Resolve(channel, out var prototype))
continue;
_channels.Add(channel);
diff --git a/Content.Client/Revolutionary/RevolutionarySystem.cs b/Content.Client/Revolutionary/RevolutionarySystem.cs
index 8e7e687fa8fc..2dc16d9c11f5 100644
--- a/Content.Client/Revolutionary/RevolutionarySystem.cs
+++ b/Content.Client/Revolutionary/RevolutionarySystem.cs
@@ -25,13 +25,13 @@ private void GetRevIcon(Entity ent, ref GetStatusIconsEv
if (HasComp(ent))
return;
- if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
+ if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
private void GetHeadRevIcon(Entity ent, ref GetStatusIconsEvent args)
{
- if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
+ if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Client/Shuttles/Systems/EmergencyShuttleSystem.cs
new file mode 100644
index 000000000000..c2b8dc8c8df0
--- /dev/null
+++ b/Content.Client/Shuttles/Systems/EmergencyShuttleSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Shuttles.Systems;
+
+namespace Content.Client.Shuttles.Systems;
+
+public sealed partial class EmergencyShuttleSystem : SharedEmergencyShuttleSystem;
diff --git a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
index 56b54c176a86..f6e6594af520 100644
--- a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
+++ b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
@@ -16,20 +16,20 @@ public bool EnableShuttlePosition
get => _enableShuttlePosition;
set
{
- if (_enableShuttlePosition == value) return;
+ if (_enableShuttlePosition == value)
+ return;
_enableShuttlePosition = value;
- var overlayManager = IoCManager.Resolve();
if (_enableShuttlePosition)
{
_overlay = new EmergencyShuttleOverlay(EntityManager.TransformQuery, XformSystem);
- overlayManager.AddOverlay(_overlay);
+ _overlays.AddOverlay(_overlay);
RaiseNetworkEvent(new EmergencyShuttleRequestPositionMessage());
}
else
{
- overlayManager.RemoveOverlay(_overlay!);
+ _overlays.RemoveOverlay(_overlay!);
_overlay = null;
}
}
diff --git a/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs b/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
index 5f77129e5e4d..b309d347e8aa 100644
--- a/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
+++ b/Content.Client/Silicons/Laws/Ui/LawDisplay.xaml.cs
@@ -26,7 +26,7 @@ public sealed partial class LawDisplay : Control
private readonly Dictionary _nextAllowedPress = new();
- public LawDisplay(EntityUid uid, SiliconLaw law, HashSet? radioChannels)
+ public LawDisplay(EntityUid uid, SiliconLaw law, HashSet>? radioChannels)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
index 77ac13c972fd..2ada6e4b0158 100644
--- a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
+++ b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
@@ -23,15 +23,15 @@ protected override void Open()
_menu.Open();
}
- private IEnumerable ConvertToButtons(IReadOnlyList actions)
+ private IEnumerable ConvertToButtons(IReadOnlyList actions)
{
- var models = new RadialMenuActionOption[actions.Count];
+ var models = new RadialMenuActionOptionBase[actions.Count];
for (int i = 0; i < actions.Count; i++)
{
var action = actions[i];
models[i] = new RadialMenuActionOption(HandleRadialMenuClick, action.Event)
{
- Sprite = action.Sprite,
+ IconSpecifier = RadialMenuIconSpecifier.With(action.Sprite),
ToolTip = action.Tooltip
};
}
diff --git a/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs
index 009969196ba0..3c5375a8e46a 100644
--- a/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs
+++ b/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs
@@ -44,7 +44,7 @@ public StationAiCustomizationMenu(EntityUid owner)
StationAiCustomizationPrototype? selectedPrototype = null;
if (stationAiCustomization?.ProtoIds.TryGetValue(groupPrototype, out var selectedProtoId) == true)
- _protoManager.TryIndex(selectedProtoId, out selectedPrototype);
+ _protoManager.Resolve(selectedProtoId, out selectedPrototype);
_buttonGroups[groupPrototype] = new ButtonGroup();
_groupContainers[groupPrototype] = new StationAiCustomizationGroupContainer(groupPrototype, selectedPrototype, _buttonGroups[groupPrototype], this, _protoManager);
@@ -76,7 +76,7 @@ public StationAiCustomizationGroupContainer
// Create UI entries for all customization in the group
foreach (var protoId in groupPrototype.ProtoIds)
{
- if (!protoManager.TryIndex(protoId, out var prototype))
+ if (!protoManager.Resolve(protoId, out var prototype))
continue;
var entry = new StationAiCustomizationEntryContainer(groupPrototype, prototype, buttonGroup, menu);
diff --git a/Content.Client/SprayPainter/SprayPainterSystem.cs b/Content.Client/SprayPainter/SprayPainterSystem.cs
index 8f7d7f03622e..788e97bef168 100644
--- a/Content.Client/SprayPainter/SprayPainterSystem.cs
+++ b/Content.Client/SprayPainter/SprayPainterSystem.cs
@@ -65,7 +65,7 @@ private void CachePrototypes()
var groupList = new List();
foreach (var groupId in category.Groups)
{
- if (!Proto.TryIndex(groupId, out var group))
+ if (!Proto.Resolve(groupId, out var group))
continue;
groupList.Add(groupId);
diff --git a/Content.Client/Stack/StackSystem.cs b/Content.Client/Stack/StackSystem.cs
index d7d1c34ae2fd..182daa73a506 100644
--- a/Content.Client/Stack/StackSystem.cs
+++ b/Content.Client/Stack/StackSystem.cs
@@ -12,7 +12,6 @@ public sealed class StackSystem : SharedStackSystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly ItemCounterSystem _counterSystem = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
diff --git a/Content.Client/Storage/Components/EntityStorageComponent.cs b/Content.Client/Storage/Components/EntityStorageComponent.cs
deleted file mode 100644
index 514d5d659575..000000000000
--- a/Content.Client/Storage/Components/EntityStorageComponent.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Content.Shared.Storage.Components;
-using Robust.Shared.GameStates;
-
-namespace Content.Client.Storage.Components;
-
-[RegisterComponent]
-public sealed partial class EntityStorageComponent : SharedEntityStorageComponent
-{
-
-}
diff --git a/Content.Client/Storage/Systems/EntityStorageSystem.cs b/Content.Client/Storage/Systems/EntityStorageSystem.cs
index dd3f8d386052..ca2b986667e6 100644
--- a/Content.Client/Storage/Systems/EntityStorageSystem.cs
+++ b/Content.Client/Storage/Systems/EntityStorageSystem.cs
@@ -31,7 +31,7 @@ public override void Initialize()
SubscribeLocalEvent(OnHandleState);
}
- public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref SharedEntityStorageComponent? component)
+ public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component)
{
if (component != null)
return true;
diff --git a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
index 351f81e3ae2b..8baa0009873a 100644
--- a/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
+++ b/Content.Client/Storage/Visualizers/EntityStorageVisualizerSystem.cs
@@ -44,7 +44,7 @@ protected override void OnAppearanceChange(EntityUid uid,
var forceRedrawBase = false;
if (AppearanceSystem.TryGetData(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
{
- if (_prototypeManager.TryIndex(prototype, out var proto))
+ if (_prototypeManager.Resolve(prototype, out var proto))
{
if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
{
diff --git a/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
index a8b93cd2b3a0..e706e74bc33e 100644
--- a/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
+++ b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
@@ -33,7 +33,7 @@ public void CreateCurrencyButtons(Dictionary, FixedPo
_validCurrencies.Clear();
foreach (var currency in balance)
{
- if (!_prototypeManager.TryIndex(currency.Key, out var proto))
+ if (!_prototypeManager.Resolve(currency.Key, out var proto))
continue;
_validCurrencies.Add(proto, currency.Value);
diff --git a/Content.Client/UserInterface/Controls/FancyWindow.xaml b/Content.Client/UserInterface/Controls/FancyWindow.xaml
index 84d0499b3a63..6195d6aea85b 100644
--- a/Content.Client/UserInterface/Controls/FancyWindow.xaml
+++ b/Content.Client/UserInterface/Controls/FancyWindow.xaml
@@ -10,7 +10,7 @@
+ HorizontalExpand="True" VAlign="Center" StyleClasses="FancyWindowTitle" ClipText="true" />
diff --git a/Content.Client/UserInterface/Controls/MenuButton.cs b/Content.Client/UserInterface/Controls/MenuButton.cs
index 540a8ecb579a..a0ba21da7446 100644
--- a/Content.Client/UserInterface/Controls/MenuButton.cs
+++ b/Content.Client/UserInterface/Controls/MenuButton.cs
@@ -25,7 +25,7 @@ public sealed class MenuButton : ContainerButton
private Color NormalColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedNormal : ColorNormal;
private Color HoveredColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedHovered : ColorHovered;
- private BoundKeyFunction _function;
+ private BoundKeyFunction? _function;
private readonly BoxContainer _root;
private readonly TextureRect? _buttonIcon;
private readonly Label? _buttonLabel;
@@ -33,13 +33,13 @@ public sealed class MenuButton : ContainerButton
public string AppendStyleClass { set => AddStyleClass(value); }
public Texture? Icon { get => _buttonIcon!.Texture; set => _buttonIcon!.Texture = value; }
- public BoundKeyFunction BoundKey
+ public BoundKeyFunction? BoundKey
{
get => _function;
set
{
_function = value;
- _buttonLabel!.Text = BoundKeyHelper.ShortKeyName(value);
+ _buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
}
@@ -95,12 +95,12 @@ protected override void ExitedTree()
private void OnKeyBindingChanged(IKeyBinding obj)
{
- _buttonLabel!.Text = BoundKeyHelper.ShortKeyName(_function);
+ _buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
private void OnKeyBindingChanged()
{
- _buttonLabel!.Text = BoundKeyHelper.ShortKeyName(_function);
+ _buttonLabel!.Text = _function == null ? "" : BoundKeyHelper.ShortKeyName(_function.Value);
}
protected override void StylePropertiesChanged()
diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs
index 9734cf296011..959a60ef4f88 100644
--- a/Content.Client/UserInterface/Controls/RadialMenu.cs
+++ b/Content.Client/UserInterface/Controls/RadialMenu.cs
@@ -229,10 +229,10 @@ public void ReturnToPreviousLayer()
/// from interactions.
///
[Virtual]
-public class RadialMenuTextureButtonBase : TextureButton
+public abstract class RadialMenuButtonBase : BaseButton
{
///
- protected RadialMenuTextureButtonBase()
+ protected RadialMenuButtonBase()
{
EnableAllKeybinds = true;
}
@@ -242,7 +242,9 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.UIClick
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
+ {
base.KeyBindUp(args);
+ }
}
}
@@ -253,8 +255,14 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
/// works only if control have parent, and ActiveContainer property is set.
/// Also considers all space outside of radial menu buttons as itself for clicking.
///
-public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase
+public sealed class RadialMenuContextualCentralTextureButton : TextureButton
{
+ ///
+ public RadialMenuContextualCentralTextureButton()
+ {
+ EnableAllKeybinds = true;
+ }
+
public float InnerRadius { get; set; }
public Vector2? ParentCenter { get; set; }
@@ -271,15 +279,25 @@ protected override bool HasPoint(Vector2 point)
var innerRadiusSquared = InnerRadius * InnerRadius;
- // comparing to squared values is faster then making sqrt
+ // comparing to squared values is faster, then making sqrt
return distSquared < innerRadiusSquared;
}
+
+ ///
+ protected override void KeyBindUp(GUIBoundKeyEventArgs args)
+ {
+ if (args.Function == EngineKeyFunctions.UIClick
+ || args.Function == ContentKeyFunctions.AltActivateItemInWorld)
+ {
+ base.KeyBindUp(args);
+ }
+ }
}
///
/// Menu button for outer area of radial menu (covers everything 'outside').
///
-public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
+public sealed class RadialMenuOuterAreaButton : RadialMenuButtonBase
{
public float OuterRadius { get; set; }
@@ -303,7 +321,7 @@ protected override bool HasPoint(Vector2 point)
}
[Virtual]
-public class RadialMenuTextureButton : RadialMenuTextureButtonBase
+public class RadialMenuButton : RadialMenuButtonBase
{
///
/// Upon clicking this button the radial menu will be moved to the layer of this control.
@@ -319,9 +337,8 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
///
/// A simple texture button that can move the user to a different layer within a radial menu
///
- public RadialMenuTextureButton()
+ public RadialMenuButton()
{
- EnableAllKeybinds = true;
OnButtonUp += OnClicked;
}
@@ -391,7 +408,7 @@ public interface IRadialMenuItemWithSector
}
[Virtual]
-public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector
+public class RadialMenuButtonWithSector : RadialMenuButton, IRadialMenuItemWithSector
{
private Vector2[]? _sectorPointsForDrawing;
@@ -500,7 +517,7 @@ float IRadialMenuItemWithSector.AngleSectorTo
///
/// A simple texture button that can move the user to a different layer within a radial menu
///
- public RadialMenuTextureButtonWithSector()
+ public RadialMenuButtonWithSector()
{
}
diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
index 31d7eab3400d..ec7dcbbb5af4 100644
--- a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
+++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
@@ -7,6 +7,8 @@
using Robust.Shared.Timing;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Input;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls;
@@ -30,7 +32,7 @@ public void Track(EntityUid owner)
_attachMenuToEntity = owner;
}
- public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null)
+ public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null)
{
ClearExistingChildrenRadialButtons();
@@ -45,7 +47,7 @@ public void OpenOverMouseScreenPosition()
}
private void Fill(
- IEnumerable models,
+ IEnumerable models,
SpriteSystem sprites,
ICollection rootControlChildren,
SimpleRadialMenuSettings settings
@@ -77,7 +79,7 @@ SimpleRadialMenuSettings settings
}
}
- private RadialMenuTextureButton RecursiveContainerExtraction(
+ private RadialMenuButton RecursiveContainerExtraction(
SpriteSystem sprites,
ICollection rootControlChildren,
RadialMenuNestedLayerOption model,
@@ -112,8 +114,8 @@ SimpleRadialMenuSettings settings
return thisLayerLinkButton;
}
- private RadialMenuTextureButton ConvertToButton(
- RadialMenuOption model,
+ private RadialMenuButton ConvertToButton(
+ RadialMenuOptionBase model,
SpriteSystem sprites,
SimpleRadialMenuSettings settings,
bool haveNested
@@ -121,29 +123,26 @@ bool haveNested
{
var button = settings.UseSectors
? ConvertToButtonWithSector(model, settings)
- : new RadialMenuTextureButton();
+ : new RadialMenuButton();
button.SetSize = new Vector2(64f, 64f);
button.ToolTip = model.ToolTip;
- if (model.Sprite != null)
+ var imageControl = model.IconSpecifier switch
{
- var scale = Vector2.One;
-
- var texture = sprites.Frame0(model.Sprite);
- if (texture.Width <= 32)
- {
- scale *= 2;
- }
+ RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites),
+ RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity),
+ RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId),
+ _ => null
+ };
- button.TextureNormal = texture;
- button.Scale = scale;
- }
+ if(imageControl != null)
+ button.AddChild(imageControl);
- if (model is RadialMenuActionOption actionOption)
+ if (model is RadialMenuActionOptionBase actionOption)
{
button.OnPressed += _ =>
{
actionOption.OnPressed?.Invoke();
- if(!haveNested)
+ if (!haveNested)
Close();
};
}
@@ -151,9 +150,53 @@ bool haveNested
return button;
}
- private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings)
+ private Control CreateEntityPrototypeView(EntProtoId protoId)
+ {
+ var entProtoView = new EntityPrototypeView
+ {
+ SetSize = new Vector2(48, 48),
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Stretch = SpriteView.StretchMode.Fill,
+ };
+ entProtoView.SetPrototype(protoId);
+ return entProtoView;
+ }
+
+ private static Control CreateSpriteView(EntityUid entityForSpriteView)
+ {
+ var entView = new SpriteView
+ {
+ SetSize = new Vector2(48, 48),
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Stretch = SpriteView.StretchMode.Fill,
+ };
+ entView.SetEntity(entityForSpriteView);
+ return entView;
+ }
+
+ private static Control CreateTexture(SpriteSpecifier spriteSpecifier, SpriteSystem sprites)
+ {
+ var scale = Vector2.One;
+
+ var texture = sprites.Frame0(spriteSpecifier);
+ if (texture.Width <= 32)
+ {
+ scale *= 2;
+ }
+
+ var imageControl = new TextureRect()
+ {
+ Texture = texture,
+ TextureScale = scale
+ };
+ return imageControl;
+ }
+
+ private static RadialMenuButtonWithSector ConvertToButtonWithSector(RadialMenuOptionBase model, SimpleRadialMenuSettings settings)
{
- var button = new RadialMenuTextureButtonWithSector
+ var button = new RadialMenuButtonWithSector
{
DrawBorder = settings.DisplayBorders,
DrawBackground = !settings.NoBackground
@@ -228,32 +271,99 @@ private void UpdatePosition()
}
+///
+/// Abstract representation of a way to specify icon in radial menu.
+///
+public abstract record RadialMenuIconSpecifier
+{
+ /// Use entity prototype viewer.
+ public static RadialMenuIconSpecifier? With(EntProtoId? protoId)
+ {
+ if (protoId is null)
+ return null;
+
+ return new RadialMenuEntityPrototypeIconSpecifier(protoId.Value);
+ }
+
+ /// Use simple texture icon.
+ public static RadialMenuIconSpecifier? With(SpriteSpecifier? sprite)
+ {
+ if (sprite == null)
+ return null;
-public abstract class RadialMenuOption
+ return new RadialMenuTextureIconSpecifier(sprite);
+ }
+
+ /// Use entity sprite viewer.
+ public static RadialMenuIconSpecifier? With(EntityUid? entity)
+ {
+ if (entity == null)
+ return null;
+
+ return new RadialMenuEntityIconSpecifier(entity.Value);
+ }
+}
+
+/// Marker that should be used to display radial menu icon.
+public sealed record RadialMenuEntityIconSpecifier(EntityUid Entity) : RadialMenuIconSpecifier;
+
+/// Marker that should be used to display radial menu icon.
+public sealed record RadialMenuTextureIconSpecifier(SpriteSpecifier Sprite) : RadialMenuIconSpecifier;
+
+/// Marker that should be used to display radial menu icon.
+public sealed record RadialMenuEntityPrototypeIconSpecifier(EntProtoId ProtoId) : RadialMenuIconSpecifier;
+
+/// Container for common options for radial menu button.
+public abstract class RadialMenuOptionBase
{
+ /// Tooltip to be displayed when button is hovered.
public string? ToolTip { get; init; }
- public SpriteSpecifier? Sprite { get; init; }
+ ///
+ /// Color for button background.
+ /// Is used only with sector radial ().
+ ///
public Color? BackgroundColor { get; set; }
+ ///
+ /// Color for button background when it is hovered.
+ /// Is used only with sector radial ().
+ ///
public Color? HoverBackgroundColor { get; set; }
+
+ ///
+ /// Specifier that describes icon to be used for radial menu button.
+ ///
+ public RadialMenuIconSpecifier? IconSpecifier { get; set; }
}
-public abstract class RadialMenuActionOption(Action onPressed) : RadialMenuOption
+/// Base type for model of radial menu button with some action on button pressed.
+///
+public abstract class RadialMenuActionOptionBase(Action onPressed) : RadialMenuOptionBase
{
+ /// Action to be executed on button press.
public Action OnPressed { get; } = onPressed;
}
-public sealed class RadialMenuActionOption(Action onPressed, T data)
- : RadialMenuActionOption(onPressed: () => onPressed(data));
+/// Strong-typed model for radial menu button with action, stores provided data to be used upon button press.
+public sealed class RadialMenuActionOption(Action onPressed, T data) : RadialMenuActionOptionBase(onPressed: () => onPressed(data));
-public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100)
- : RadialMenuOption
+///
+/// Model for radial menu button that represents reference for next layer of radial buttons.
+///
+/// List of button models for next layer of menu.
+/// Radius for radial menu buttons of next layer.
+public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) : RadialMenuOptionBase
{
+ /// Radius for radial menu buttons of next layer.
public float? ContainerRadius { get; } = containerRadius;
- public IReadOnlyCollection Nested { get; } = nested;
+ /// List of button models for next layer of menu.
+ public IReadOnlyCollection Nested { get; } = nested;
}
+///
+/// Additional settings for radial menu render.
+///
public sealed class SimpleRadialMenuSettings
{
///
diff --git a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
index fb53ba021f83..bd9910e47167 100644
--- a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
+++ b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
@@ -17,6 +17,7 @@
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
+using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -38,6 +39,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged ToggleWindow()))
- .Register();
+ _input.SetInputCommand(ContentKeyFunctions.OpenAHelp,
+ InputCmdHandler.FromDelegate(_ => ToggleWindow()));
}
public void OnSystemUnloaded(BwoinkSystem system)
{
- CommandBinds.Unregister();
+ _input.SetInputCommand(ContentKeyFunctions.OpenAHelp, null);
DebugTools.Assert(_bwoinkSystem != null);
_bwoinkSystem!.OnBwoinkTextMessageRecieved -= ReceivedBwoink;
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
index c6472464c54d..c5f2337b3426 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.Highlighting.cs
@@ -116,8 +116,9 @@ public void UpdateHighlights(string newHighlights, bool firstLoad = false)
keyword = EndDoubleQuote.Replace(keyword, "(?
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!; // Starlight
@@ -134,12 +133,12 @@ private void CloseMenu()
_menu = null;
}
- private IEnumerable ConvertToButtons(IEnumerable emotePrototypes)
+ private IEnumerable ConvertToButtons(IEnumerable emotePrototypes)
{
var whitelistSystem = EntitySystemManager.GetEntitySystem();
var player = _playerManager.LocalSession?.AttachedEntity;
- Dictionary> emotesByCategory = new();
+ Dictionary> emotesByCategory = new();
foreach (var emote in emotePrototypes)
{
if (emote.Category == EmoteCategory.Invalid)
@@ -159,13 +158,13 @@ private IEnumerable ConvertToButtons(IEnumerable();
+ list = new List();
emotesByCategory.Add(emote.Category, list);
}
var actionOption = new RadialMenuActionOption(HandleRadialButtonClick, emote)
{
- Sprite = emote.Icon,
+ IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon),
ToolTip = Loc.GetString(emote.Name)
};
list.Add(actionOption);
@@ -177,20 +176,20 @@ private IEnumerable ConvertToButtons(IEnumerable();
+ list = new List();
emotesByCategory.Add(EmoteCategory.Cloud, list);
}
var actionOption = new RadialMenuActionOption(HandleCloudRadialButtonClick, emote)
{
- Sprite = emote.Icon,
+ IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon),
ToolTip = Loc.GetString(emote.ID)
};
list.Add(actionOption);
}
// Starlight - End
- var models = new RadialMenuOption[emotesByCategory.Count];
+ var models = new RadialMenuOptionBase[emotesByCategory.Count];
var i = 0;
foreach (var (key, list) in emotesByCategory)
{
@@ -198,7 +197,7 @@ private IEnumerable ConvertToButtons(IEnumerable ConvertToButtons(IEnumerable inventory, bool enabled,
{
var entry = inventory[i];
- if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
+ if (!_prototypeManager.Resolve(entry.ID, out var prototype))
{
_amounts[entry.ID] = 0;
continue;
diff --git a/Content.Client/Verbs/UI/VerbMenuUIController.cs b/Content.Client/Verbs/UI/VerbMenuUIController.cs
index efacf877adee..32da302904fc 100644
--- a/Content.Client/Verbs/UI/VerbMenuUIController.cs
+++ b/Content.Client/Verbs/UI/VerbMenuUIController.cs
@@ -273,7 +273,7 @@ public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
if (verbElement.SubMenu == null)
{
- var popupElement = new ConfirmationMenuElement(verb, "Confirm");
+ var popupElement = new ConfirmationMenuElement(verb, Loc.GetString("generic-confirm"));
verbElement.SubMenu = new ContextMenuPopup(_context, verbElement);
_context.AddElement(verbElement.SubMenu, popupElement);
}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
index 75a595813212..7165bf763026 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
@@ -41,6 +41,7 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
@@ -73,11 +74,10 @@ public bool SpreadOverlay
return;
_spreadOverlay = value;
- var overlayManager = IoCManager.Resolve();
if (_spreadOverlay)
{
- overlayManager.AddOverlay(new GunSpreadOverlay(
+ _overlayManager.AddOverlay(new GunSpreadOverlay(
EntityManager,
_eyeManager,
Timing,
@@ -88,7 +88,7 @@ public bool SpreadOverlay
}
else
{
- overlayManager.RemoveOverlay();
+ _overlayManager.RemoveOverlay();
}
}
}
diff --git a/Content.Client/Wieldable/WieldableSystem.cs b/Content.Client/Wieldable/WieldableSystem.cs
index 2de837923c87..e40544e39dc3 100644
--- a/Content.Client/Wieldable/WieldableSystem.cs
+++ b/Content.Client/Wieldable/WieldableSystem.cs
@@ -29,7 +29,10 @@ public void OnEyeOffsetUnwielded(Entity enti
return;
if (_gameTiming.IsFirstTimePredicted)
+ {
cursorOffsetComp.CurrentPosition = Vector2.Zero;
+ cursorOffsetComp.TargetPosition = Vector2.Zero;
+ }
}
public void OnGetEyeOffset(Entity entity, ref HeldRelayedEvent args)
diff --git a/Content.Client/_Starlight/Antags/TerrorSpider/EggsLayingMenu.xaml.cs b/Content.Client/_Starlight/Antags/TerrorSpider/EggsLayingMenu.xaml.cs
index 3e2e8ee75c15..5e9efaec4b15 100644
--- a/Content.Client/_Starlight/Antags/TerrorSpider/EggsLayingMenu.xaml.cs
+++ b/Content.Client/_Starlight/Antags/TerrorSpider/EggsLayingMenu.xaml.cs
@@ -62,7 +62,7 @@ private void AddButton(RadialContainer main, string tooltip, Texture sprite, str
}
}
-public sealed class MenuButton : RadialMenuTextureButton
+public sealed class MenuButton : RadialMenuButton
{
public MenuButton()
{
diff --git a/Content.Client/_Starlight/Crayon/Overlays/CrayonDecalGhostOverlay.cs b/Content.Client/_Starlight/Crayon/Overlays/CrayonDecalGhostOverlay.cs
new file mode 100644
index 000000000000..9a557fb1d8a3
--- /dev/null
+++ b/Content.Client/_Starlight/Crayon/Overlays/CrayonDecalGhostOverlay.cs
@@ -0,0 +1,51 @@
+using Content.Client.Decals;
+using Content.Client.Decals.Overlays;
+using Content.Shared.Decals;
+using Content.Shared.Interaction;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Player;
+
+namespace Content.Client._Starlight.Crayon.Overlays;
+
+public sealed class CrayonDecalGhostOverlay : DecalPlacementOverlay
+{
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ private readonly SharedInteractionSystem _interaction;
+ private readonly DecalPrototype? _decalPrototype;
+ private readonly Angle _rotation;
+ private readonly Color _color;
+
+ public CrayonDecalGhostOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite, SharedInteractionSystem interaction, DecalPrototype? decalPrototype, Angle rotation, Color color) : base(placement, transform, sprite)
+ {
+ IoCManager.InjectDependencies(this);
+ _interaction = interaction;
+ _decalPrototype = decalPrototype;
+ _rotation = rotation;
+ _color = color;
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ var mouseScreenPos = _inputManager.MouseScreenPosition;
+ var mousePos = _eyeManager.PixelToMap(mouseScreenPos);
+
+ var player = _playerManager.LocalEntity;
+ if (player is null)
+ return false;
+ return _interaction.InRangeUnobstructed(mousePos,
+ player.Value,
+ collisionMask: Shared.Physics.CollisionGroup.None);
+ }
+
+ protected override void LoadDecal()
+ {
+ decal = _decalPrototype;
+ snap = false;
+ rotation = _rotation;
+ color = _color;
+ }
+}
diff --git a/Content.Client/_Starlight/Thaven/Eui/MoodContainer.xaml b/Content.Client/_Starlight/Thaven/Eui/MoodContainer.xaml
new file mode 100644
index 000000000000..a92b1d50451e
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/Eui/MoodContainer.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Starlight/Thaven/Eui/MoodContainer.xaml.cs b/Content.Client/_Starlight/Thaven/Eui/MoodContainer.xaml.cs
new file mode 100644
index 000000000000..d989aadfd707
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/Eui/MoodContainer.xaml.cs
@@ -0,0 +1,33 @@
+using Content.Shared._Starlight.Thaven;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Starlight.Thaven.Eui;
+
+[GenerateTypedNameReferences]
+public sealed partial class MoodContainer : BoxContainer
+{
+ public event Action? OnMoveUp;
+ public event Action? OnMoveDown;
+ public event Action? OnDelete;
+
+ public MoodContainer(ThavenMood? mood = null)
+ {
+ RobustXamlLoader.Load(this);
+
+ if (mood != null)
+ {
+ ThavenMoodTitle.Text = mood.GetLocName();
+ ThavenMoodContent.TextRope = new Rope.Leaf(mood.GetLocDesc());
+ }
+
+ MoveUp.OnPressed += _ => OnMoveUp?.Invoke();
+ MoveDown.OnPressed += _ => OnMoveDown?.Invoke();
+ Delete.OnPressed += _ => OnDelete?.Invoke();
+ }
+
+ public string MoodTitle => ThavenMoodTitle.Text;
+ public string MoodText => Rope.Collapse(ThavenMoodContent.TextRope).Trim();
+}
diff --git a/Content.Client/_Starlight/Thaven/Eui/ThavenMoodUi.xaml b/Content.Client/_Starlight/Thaven/Eui/ThavenMoodUi.xaml
new file mode 100644
index 000000000000..a66480d2a4ec
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/Eui/ThavenMoodUi.xaml
@@ -0,0 +1,20 @@
+
+
+ this shit does not layout properly unless I put the horizontal boxcontainer inside of a vertical one
+ ????
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Starlight/Thaven/Eui/ThavenMoodUi.xaml.cs b/Content.Client/_Starlight/Thaven/Eui/ThavenMoodUi.xaml.cs
new file mode 100644
index 000000000000..a2213ed00e04
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/Eui/ThavenMoodUi.xaml.cs
@@ -0,0 +1,96 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Starlight.Thaven;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Starlight.Thaven.Eui;
+
+[GenerateTypedNameReferences]
+public sealed partial class ThavenMoodUi : FancyWindow
+{
+ public event Action? OnSave;
+
+ private List _moods = new();
+
+ public ThavenMoodUi()
+ {
+ RobustXamlLoader.Load(this);
+ NewMoodButton.OnPressed += _ => AddNewMood();
+ SaveButton.OnPressed += _ => OnSave?.Invoke();
+ }
+
+ private void AddNewMood()
+ {
+ MoodContainer.AddChild(new MoodContainer());
+ }
+
+ public List GetMoods()
+ {
+ var newMoods = new List();
+
+ foreach (var control in MoodContainer.Children)
+ {
+ if (control is not MoodContainer moodControl)
+ continue;
+
+ var title = moodControl.MoodTitle;
+ if (string.IsNullOrWhiteSpace(title))
+ continue;
+
+ var moodText = moodControl.MoodText;
+ if (string.IsNullOrWhiteSpace(moodText))
+ continue;
+
+ var mood = new ThavenMood()
+ {
+ MoodName = title,
+ MoodDesc = moodText,
+ };
+
+ newMoods.Add(mood);
+ }
+
+ return newMoods;
+ }
+
+ private void MoveUp(int index)
+ {
+ if (index <= 0)
+ return;
+
+ (_moods[index], _moods[index - 1]) = (_moods[index - 1], _moods[index]);
+ SetMoods(_moods);
+ }
+
+ private void MoveDown(int index)
+ {
+ if (index >= _moods.Count - 1)
+ return;
+
+ (_moods[index], _moods[index + 1]) = (_moods[index + 1], _moods[index]);
+ SetMoods(_moods);
+ }
+
+ private void Delete(int index)
+ {
+ _moods.RemoveAt(index);
+
+ SetMoods(_moods);
+ }
+
+ public void SetMoods(List moods)
+ {
+ _moods = moods;
+ MoodContainer.RemoveAllChildren();
+ for (var i = 0; i < moods.Count; i++)
+ {
+ var index = i; // Copy for the closures
+ var moodControl = new MoodContainer(moods[i]);
+ moodControl.OnMoveUp += () => MoveUp(index);
+ moodControl.OnMoveDown += () => MoveDown(index);
+ moodControl.OnDelete += () => Delete(index);
+ MoodContainer.AddChild(moodControl);
+ }
+ }
+}
diff --git a/Content.Client/_Starlight/Thaven/Eui/ThavenMoodsEui.cs b/Content.Client/_Starlight/Thaven/Eui/ThavenMoodsEui.cs
new file mode 100644
index 000000000000..0f03311155cb
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/Eui/ThavenMoodsEui.cs
@@ -0,0 +1,38 @@
+using Content.Client.Eui;
+using Content.Shared.Eui;
+using Content.Shared._Starlight.Thaven;
+
+namespace Content.Client._Starlight.Thaven.Eui;
+
+public sealed class ThavenMoodsEui : BaseEui
+{
+ private ThavenMoodUi _thavenMoodUi;
+ private NetEntity _target;
+
+ public ThavenMoodsEui()
+ {
+ _thavenMoodUi = new ThavenMoodUi();
+ _thavenMoodUi.OnSave += SaveMoods;
+ }
+
+ private void SaveMoods()
+ {
+ var newMoods = _thavenMoodUi.GetMoods();
+ SendMessage(new ThavenMoodsSaveMessage(newMoods, _target));
+ _thavenMoodUi.SetMoods(newMoods);
+ }
+
+ public override void Opened()
+ {
+ _thavenMoodUi.OpenCentered();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not ThavenMoodsEuiState s)
+ return;
+
+ _target = s.Target;
+ _thavenMoodUi.SetMoods(s.Moods);
+ }
+}
diff --git a/Content.Client/_Starlight/Thaven/MoodDisplay.xaml b/Content.Client/_Starlight/Thaven/MoodDisplay.xaml
new file mode 100644
index 000000000000..b853cac5b646
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/MoodDisplay.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Starlight/Thaven/MoodDisplay.xaml.cs b/Content.Client/_Starlight/Thaven/MoodDisplay.xaml.cs
new file mode 100644
index 000000000000..593d991500ce
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/MoodDisplay.xaml.cs
@@ -0,0 +1,29 @@
+using Content.Client.Message;
+using Content.Shared._Starlight.Thaven;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+
+namespace Content.Client._Starlight.Thaven;
+
+[GenerateTypedNameReferences]
+public sealed partial class MoodDisplay : Control
+{
+ private string GetSharedString()
+ {
+ return $"[italic][font size=10][color=gray]{Loc.GetString("moods-ui-shared-mood")}[/color][/font][/italic]";
+ }
+
+ public MoodDisplay(ThavenMood mood, bool shared)
+ {
+ RobustXamlLoader.Load(this);
+
+ var name = mood.GetLocName();
+ if (shared)
+ MoodNameLabel.SetMarkup($"{name} {GetSharedString()}");
+ else
+ MoodNameLabel.SetMarkup(name);
+ MoodDescLabel.SetMarkup(mood.GetLocDesc());
+ }
+}
diff --git a/Content.Client/_Starlight/Thaven/ThavenMoodSystem.cs b/Content.Client/_Starlight/Thaven/ThavenMoodSystem.cs
new file mode 100644
index 000000000000..2722edfa6c60
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/ThavenMoodSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared._Starlight.Thaven;
+
+namespace Content.Client._Starlight.Thaven;
+
+public sealed partial class ThavenMoodSystem : SharedThavenMoodSystem;
diff --git a/Content.Client/_Starlight/Thaven/ThavenMoodsBoundUserInterface.cs b/Content.Client/_Starlight/Thaven/ThavenMoodsBoundUserInterface.cs
new file mode 100644
index 000000000000..4327708d2a00
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/ThavenMoodsBoundUserInterface.cs
@@ -0,0 +1,39 @@
+using Content.Shared._Starlight.Thaven;
+using Content.Shared._Starlight.Thaven.Components;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client._Starlight.Thaven;
+
+[UsedImplicitly]
+public sealed class ThavenMoodsBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ [ViewVariables]
+ private ThavenMoodsMenu? _menu;
+
+ public ThavenMoodsBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = this.CreateWindow();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not ThavenMoodsBuiState msg)
+ return;
+
+ if (!_entMan.TryGetComponent(Owner, out var comp))
+ return;
+
+ _menu?.Update(comp, msg);
+ }
+}
diff --git a/Content.Client/_Starlight/Thaven/ThavenMoodsMenu.xaml b/Content.Client/_Starlight/Thaven/ThavenMoodsMenu.xaml
new file mode 100644
index 000000000000..7de1a20dbc65
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/ThavenMoodsMenu.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Starlight/Thaven/ThavenMoodsMenu.xaml.cs b/Content.Client/_Starlight/Thaven/ThavenMoodsMenu.xaml.cs
new file mode 100644
index 000000000000..97bd44e667b9
--- /dev/null
+++ b/Content.Client/_Starlight/Thaven/ThavenMoodsMenu.xaml.cs
@@ -0,0 +1,27 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Starlight.Thaven.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Starlight.Thaven;
+
+[GenerateTypedNameReferences]
+public sealed partial class ThavenMoodsMenu : FancyWindow
+{
+ public ThavenMoodsMenu()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void Update(ThavenMoodsComponent comp, ThavenMoodsBuiState state)
+ {
+ MoodDisplayContainer.Children.Clear();
+
+ foreach (var mood in state.SharedMoods)
+ MoodDisplayContainer.AddChild(new MoodDisplay(mood, true));
+
+ foreach (var mood in comp.Moods)
+ MoodDisplayContainer.AddChild(new MoodDisplay(mood, false));
+ }
+}
diff --git a/Content.Client/_Starlight/UserInterface/RichText/CheckTagHandler.cs b/Content.Client/_Starlight/UserInterface/RichText/CheckTagHandler.cs
new file mode 100644
index 000000000000..076d4b9c3cf6
--- /dev/null
+++ b/Content.Client/_Starlight/UserInterface/RichText/CheckTagHandler.cs
@@ -0,0 +1,136 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Robust.Client.UserInterface.RichText;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Utility;
+using Content.Client.Paper.UI;
+
+namespace Content.Client.UserInterface.RichText;
+
+///
+/// Converts [check] tags into clickable buttons that toggle between ✔ and ✖.
+///
+public sealed class CheckTagHandler : IMarkupTagHandler
+{
+ public string Name => "check";
+ private static int _checkCounter = 0;
+
+ ///
+ /// Font line height set by PaperWindow to ensure buttons match text height
+ ///
+ public static float FontLineHeight { get; set; } = 16.0f; // Default fallback
+
+ private static int GetCheckIndex(MarkupNode node) => _checkCounter++;
+
+ ///
+ /// Resets the check counter to ensure consistent indexing across renders.
+ ///
+ public static void ResetCheckCounter() => _checkCounter = 0;
+
+ ///
+ /// Counts check buttons before the clicked button to determine which [check] tag it represents.
+ ///
+ private static int CountCheckButtonsBefore(Control clickedButton)
+ {
+ var count = 0;
+ var root = clickedButton;
+
+ // Find the root container
+ while (root.Parent != null)
+ root = root.Parent;
+
+ // Count check buttons in document order
+ var found = false;
+ CountCheckButtonsRecursive(root, clickedButton, ref count, ref found);
+ return found ? count : 0;
+ }
+
+ private static void CountCheckButtonsRecursive(Control control, Control target, ref int count, ref bool found)
+ {
+ if (found) return;
+
+ if (control is Button btn && (btn.Text == "☐" || btn.Text == "✔" || btn.Text == "✖"))
+ {
+ if (control == target)
+ {
+ found = true;
+ return;
+ }
+ count++;
+ }
+
+ foreach (Control child in control.Children)
+ {
+ CountCheckButtonsRecursive(child, target, ref count, ref found);
+ }
+ }
+
+ public void PushDrawContext(MarkupNode node, MarkupDrawingContext context) { }
+ public void PopDrawContext(MarkupNode node, MarkupDrawingContext context) { }
+ public string TextBefore(MarkupNode node) => "";
+ public string TextAfter(MarkupNode node) => "";
+
+ ///
+ /// Creates a clickable button to replace the [check] tag.
+ ///
+ public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
+ {
+ var btn = new Button
+ {
+ Text = "☐",
+ MinSize = new Vector2(FontLineHeight + 2, FontLineHeight + 2),
+ MaxSize = new Vector2(FontLineHeight + 2, FontLineHeight + 2),
+ Margin = new Thickness(1, 0, 1, 0),
+ StyleClasses = { "ButtonSquare" },
+ TextAlign = Label.AlignMode.Center
+ };
+
+ var checkIndex = GetCheckIndex(node);
+ btn.Name = $"check_{checkIndex}";
+
+ btn.OnPressed += _ =>
+ {
+ // Find the PaperWindow parent
+ var parent = btn.Parent;
+ while (parent != null && parent is not PaperWindow)
+ parent = parent.Parent;
+
+ if (parent is PaperWindow paperWindow)
+ {
+ // Count buttons to determine which [check] tag this represents
+ var buttonIndex = CountCheckButtonsBefore(btn);
+ paperWindow.OpenCheckDialog(buttonIndex);
+ }
+ };
+
+ control = btn;
+ return true;
+ }
+
+ ///
+ /// Replaces the nth occurrence of [check] tag with replacement symbol.
+ ///
+ private static string ReplaceNthCheckTag(string text, int index, string replacement)
+ {
+ const string checkTag = "[check]";
+ var currentIndex = 0;
+ var pos = 0;
+
+ while (pos < text.Length)
+ {
+ var foundPos = text.IndexOf(checkTag, pos);
+ if (foundPos == -1) break;
+
+ if (currentIndex == index)
+ {
+ return text.Substring(0, foundPos) + replacement + text.Substring(foundPos + checkTag.Length);
+ }
+
+ currentIndex++;
+ pos = foundPos + checkTag.Length;
+ }
+
+ return text;
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_Starlight/UserInterface/RichText/FormTagHandler.cs b/Content.Client/_Starlight/UserInterface/RichText/FormTagHandler.cs
new file mode 100644
index 000000000000..74aefaab97ac
--- /dev/null
+++ b/Content.Client/_Starlight/UserInterface/RichText/FormTagHandler.cs
@@ -0,0 +1,135 @@
+using System.Numerics;
+using System.Diagnostics.CodeAnalysis;
+using Robust.Client.UserInterface.RichText;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface;
+using Robust.Shared.Utility;
+using Content.Client.Paper.UI;
+using Robust.Client.Graphics;
+
+namespace Content.Client.UserInterface.RichText;
+
+///
+/// Converts [form] tags into clickable buttons that open fill-in dialogs.
+///
+public sealed class FormTagHandler : IMarkupTagHandler
+{
+ public string Name => "form";
+
+ public bool CanHandle(MarkupNode node) => node.Name == "form" || node.Value.StringValue?.StartsWith("__FORM_") == true;
+
+ private static int _formCounter = 0;
+ private static readonly Dictionary _formPositions = new();
+ private static string _lastText = "";
+
+ ///
+ /// Font line height set by PaperWindow to ensure buttons match text height
+ ///
+ public static float FontLineHeight { get; set; } = 16.0f; // Default fallback
+
+ private static int GetFormIndex(MarkupNode node) => _formCounter++;
+
+ ///
+ /// Resets the form counter to ensure consistent indexing across renders.
+ ///
+ public static void ResetFormCounter() => _formCounter = 0;
+
+ ///
+ /// Counts form buttons before the clicked button to determine which [form] tag it represents.
+ ///
+ private static int CountFormButtonsBefore(Control clickedButton)
+ {
+ var count = 0;
+ var root = clickedButton;
+
+ // Find the root container
+ while (root.Parent != null)
+ root = root.Parent;
+
+ // Count form buttons in document order
+ var found = false;
+ CountFormButtonsRecursive(root, clickedButton, ref count, ref found);
+ return found ? count : 0;
+ }
+
+ private static void CountFormButtonsRecursive(Control control, Control target, ref int count, ref bool found)
+ {
+ if (found) return;
+
+ if (control is Button btn && btn.Text == Loc.GetString("paper-form-fill-button"))
+ {
+ if (control == target)
+ {
+ found = true;
+ return;
+ }
+ count++;
+ }
+
+ foreach (Control child in control.Children)
+ {
+ CountFormButtonsRecursive(child, target, ref count, ref found);
+ }
+ }
+
+ ///
+ /// Caches form tag positions to avoid recalculating on every render.
+ ///
+ public static void SetFormText(string text)
+ {
+ if (_lastText != text)
+ {
+ _formPositions.Clear();
+ _lastText = text;
+ var pos = 0;
+ var index = 0;
+ while ((pos = text.IndexOf("[form]", pos)) != -1)
+ {
+ _formPositions[pos.ToString()] = index++;
+ pos += 6;
+ }
+ }
+ }
+
+ public void PushDrawContext(MarkupNode node, MarkupDrawingContext context) { }
+ public void PopDrawContext(MarkupNode node, MarkupDrawingContext context) { }
+ public string TextBefore(MarkupNode node) => "";
+ public string TextAfter(MarkupNode node) => "";
+
+ ///
+ /// Creates a clickable button to replace the [form] tag.
+ ///
+ public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
+ {
+ var btn = new Button
+ {
+ Text = "Fill",
+ MinSize = new Vector2(32, FontLineHeight + 2),
+ MaxSize = new Vector2(32, FontLineHeight + 2),
+ Margin = new Thickness(1, 0, 1, 0),
+ StyleClasses = { "ButtonSquare" },
+ TextAlign = Label.AlignMode.Center
+ };
+
+ var formIndex = GetFormIndex(node);
+ btn.Name = $"form_{formIndex}";
+
+ btn.OnPressed += _ =>
+ {
+ // Find the PaperWindow parent
+ var parent = btn.Parent;
+ while (parent != null && parent is not PaperWindow)
+ parent = parent.Parent;
+
+ if (parent is PaperWindow paperWindow)
+ {
+ // Count buttons to determine which [form] tag this represents
+ var buttonIndex = CountFormButtonsBefore(btn);
+ paperWindow.OpenFormDialog(buttonIndex);
+ }
+ };
+
+ control = btn;
+ return true;
+ }
+}
diff --git a/Content.Client/_Starlight/UserInterface/RichText/SignatureTagHandler.cs b/Content.Client/_Starlight/UserInterface/RichText/SignatureTagHandler.cs
new file mode 100644
index 000000000000..a2d70408c793
--- /dev/null
+++ b/Content.Client/_Starlight/UserInterface/RichText/SignatureTagHandler.cs
@@ -0,0 +1,120 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Robust.Client.UserInterface.RichText;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Utility;
+using Robust.Shared.IoC;
+using Content.Client.Paper.UI;
+using Robust.Client.Graphics;
+
+namespace Content.Client.UserInterface.RichText;
+
+///
+/// Converts [signature] tags into clickable buttons that sign with the player's name.
+///
+public sealed class SignatureTagHandler : IMarkupTagHandler
+{
+ public string Name => "signature";
+ private static int _signatureCounter = 0;
+
+ ///
+ /// Font line height set by PaperWindow to ensure buttons match text height
+ ///
+ public static float FontLineHeight { get; set; } = 16.0f; // Default fallback
+
+ private static int GetSignatureIndex(MarkupNode node) => _signatureCounter++;
+
+ ///
+ /// Resets the signature counter to ensure consistent indexing across renders.
+ ///
+ public static void ResetSignatureCounter() => _signatureCounter = 0;
+
+ ///
+ /// Counts signature buttons before the clicked button to determine which [signature] tag it represents.
+ ///
+ private static int CountSignatureButtonsBefore(Control clickedButton)
+ {
+ var count = 0;
+ var root = clickedButton;
+
+ // Find the root container
+ while (root.Parent != null)
+ root = root.Parent;
+
+ // Count signature buttons in document order
+ var found = false;
+ CountSignatureButtonsRecursive(root, clickedButton, ref count, ref found);
+ return found ? count : 0;
+ }
+
+ private static void CountSignatureButtonsRecursive(Control control, Control target, ref int count, ref bool found)
+ {
+ if (found) return;
+
+ if (control is Button btn && btn.Text == Loc.GetString("paper-signature-sign-button"))
+ {
+ if (control == target)
+ {
+ found = true;
+ return;
+ }
+ count++;
+ }
+
+ foreach (Control child in control.Children)
+ {
+ CountSignatureButtonsRecursive(child, target, ref count, ref found);
+ }
+ }
+
+ public SignatureTagHandler()
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void PushDrawContext(MarkupNode node, MarkupDrawingContext context) { }
+ public void PopDrawContext(MarkupNode node, MarkupDrawingContext context) { }
+ public string TextBefore(MarkupNode node) => "";
+ public string TextAfter(MarkupNode node) => "";
+
+ ///
+ /// Creates a clickable signature button to replace the [signature] tag.
+ ///
+ public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
+ {
+ var btn = new Button
+ {
+ Text = "Sign",
+ MinSize = new Vector2(48, FontLineHeight + 4),
+ MaxSize = new Vector2(48, FontLineHeight + 4),
+ Margin = new Thickness(1, 2, 1, 2),
+ StyleClasses = { "ButtonSquare" },
+ TextAlign = Label.AlignMode.Center
+ };
+
+ var signatureIndex = GetSignatureIndex(node);
+ btn.Name = $"signature_{signatureIndex}";
+
+ btn.OnPressed += _ =>
+ {
+ // Find the PaperWindow parent
+ var parent = btn.Parent;
+ while (parent != null && parent is not PaperWindow)
+ parent = parent.Parent;
+
+ if (parent is PaperWindow paperWindow)
+ {
+ // Count buttons to determine which [signature] tag this represents
+ var buttonIndex = CountSignatureButtonsBefore(btn);
+ // Send signature request to server instead of handling client-side
+ paperWindow.SendSignatureRequest(buttonIndex);
+ }
+ };
+
+ control = btn;
+ return true;
+ }
+
+
+}
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
index 2c51bdbc3a3c..8cf2b626dc6f 100644
--- a/Content.IntegrationTests/PoolManager.Cvars.cs
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -21,6 +21,7 @@ private static readonly (string cvar, string value)[] TestCvars =
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
+ (CCVars.GameRoleLoadoutTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"),
diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
new file mode 100644
index 000000000000..9dda130847d5
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
@@ -0,0 +1,417 @@
+using System.Linq;
+using System.Numerics;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Robust.Shared.EntitySerialization;
+using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
+/// handling the DeltaPressureComponent.
+///
+[TestFixture]
+[TestOf(typeof(DeltaPressureSystem))]
+public sealed class DeltaPressureTest
+{
+ #region Prototypes
+
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ parent: BaseStructure
+ id: DeltaPressureSolidTest
+ placement:
+ mode: SnapgridCenter
+ snap:
+ - Wall
+ components:
+ - type: Physics
+ bodyType: Static
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: ""-0.5,-0.5,0.5,0.5""
+ mask:
+ - FullTileMask
+ layer:
+ - WallLayer
+ density: 1000
+ - type: Airtight
+ - type: DeltaPressure
+ minPressure: 15000
+ minPressureDelta: 10000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+ - type: Damageable
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 300
+ behaviors:
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ Girder:
+ min: 1
+ max: 1
+ - !type:DoActsBehavior
+ acts: [ ""Destruction"" ]
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestNoAutoJoin
+ components:
+ - type: DeltaPressure
+ autoJoinProcessingList: false
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestAbsolute
+ components:
+ - type: DeltaPressure
+ minPressure: 10000
+ minPressureDelta: 15000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+";
+
+ #endregion
+
+ private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ ///
+ /// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to true is automatically added to the DeltaPressure processing list
+ /// on the grid's GridAtmosphereComponent.
+ ///
+ /// Also asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to false is not automatically added to the DeltaPressure processing list.
+ ///
+ [Test]
+ public async Task ProcessingListAutoJoinTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ });
+
+ await server.WaitAssertion(() =>
+ {
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
+ entMan.DeleteEntity(uid);
+ Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+ /// is not damaged by DeltaPressure.
+ ///
+ [Test]
+ public async Task ProcessingDeltaStandbyTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressureDelta - 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ // Entity should exist, if it took one tick of damage then it should be instantly destroyed.
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that needs to be damaged by DeltaPressure
+ /// is damaged by DeltaPressure when the pressure is above the threshold.
+ ///
+ [Test]
+ public async Task ProcessingDeltaDamageTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ // Need to spawn an entity each run to ensure it works for all directions.
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressureDelta + 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ // Entity should exist, if it took one tick of damage then it should be instantly destroyed.
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+ /// is not damaged by DeltaPressure when using absolute pressure thresholds.
+ ///
+ [Test]
+ public async Task ProcessingAbsoluteStandbyTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+ grid = gridSet.First();
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressure - 10; // just below absolute threshold
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that needs to be damaged by DeltaPressure
+ /// is damaged by DeltaPressure when the pressure is above the absolute threshold.
+ ///
+ [Test]
+ public async Task ProcessingAbsoluteDamageTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+ grid = gridSet.First();
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ // Spawn fresh entity each iteration to verify all directions work
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ // Above absolute threshold but below delta threshold to ensure absolute alone causes damage
+ var toPressurize = dpEnt.Comp!.MinPressure + 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
new file mode 100644
index 000000000000..07caf447bd9c
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs
@@ -0,0 +1,85 @@
+using System.Linq;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+[TestFixture]
+[TestOf(typeof(Atmospherics))]
+public sealed class GasArrayTest
+{
+ private const string GasTankTestDummyId = "GasTankTestDummy";
+
+ private const string GasTankLegacyTestDummyId = "GasTankLegacyTestDummy";
+
+ [TestPrototypes]
+ private const string Prototypes = $@"
+- type: entity
+ id: {GasTankTestDummyId}
+ components:
+ - type: GasTank
+ air:
+ volume: 5
+ moles:
+ Frezon: 20
+ Oxygen: 10
+
+- type: entity
+ id: {GasTankLegacyTestDummyId}
+ components:
+ - type: GasTank
+ air:
+ volume: 5
+ moles:
+ - 0
+ - 0
+ - 0
+ - 10
+";
+
+ [Test]
+ public async Task TestGasArrayDeserialization()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var compFactory = server.ResolveDependency();
+ var prototypeManager = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ var gasTank = prototypeManager.Index(GasTankTestDummyId);
+ Assert.Multiple(() =>
+ {
+ Assert.That(gasTank.TryGetComponent(out var gasTankComponent, compFactory));
+
+ Assert.That(gasTankComponent!.Air.GetMoles(Gas.Oxygen), Is.EqualTo(10));
+ Assert.That(gasTankComponent!.Air.GetMoles(Gas.Frezon), Is.EqualTo(20));
+ foreach (var gas in Enum.GetValues().Where(p => p != Gas.Oxygen && p != Gas.Frezon))
+ {
+ Assert.That(gasTankComponent!.Air.GetMoles(gas), Is.EqualTo(0));
+ }
+ });
+
+ var legacyGasTank = prototypeManager.Index(GasTankLegacyTestDummyId);
+ Assert.Multiple(() =>
+ {
+ Assert.That(legacyGasTank.TryGetComponent(out var gasTankComponent, compFactory));
+
+ Assert.That(gasTankComponent!.Air.GetMoles(3), Is.EqualTo(10));
+
+ // Iterate through all other gases: check for 0 values
+ for (var i = 0; i < Atmospherics.AdjustedNumberOfGases; i++)
+ {
+ if (i == 3) // our case with a value.
+ continue;
+
+ Assert.That(gasTankComponent!.Air.GetMoles(i), Is.EqualTo(0));
+ }
+ });
+ });
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs
index e5f9fa1e815b..aad87b711a4e 100644
--- a/Content.IntegrationTests/Tests/CargoTest.cs
+++ b/Content.IntegrationTests/Tests/CargoTest.cs
@@ -6,6 +6,7 @@
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Cargo.Prototypes;
+using Content.Shared.Mobs.Components;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
@@ -250,4 +251,25 @@ await server.WaitAssertion(() =>
await pair.CleanReturnAsync();
}
+
+ [Test]
+ public async Task MobPrice()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+
+ var componentFactory = pair.Server.ResolveDependency();
+
+ await pair.Server.WaitAssertion(() =>
+ {
+ Assert.Multiple(() =>
+ {
+ foreach (var (proto, comp) in pair.GetPrototypesWithComponent())
+ {
+ Assert.That(proto.TryGetComponent(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no MobStateComponent!");
+ }
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
}
diff --git a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
index b480ed72c7a4..6e1d4d5defa0 100644
--- a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
+++ b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
@@ -147,8 +147,8 @@ await server.WaitPost(() =>
mobThresholdsComp = entManager.GetComponent(player);
damageableComp = entManager.GetComponent(player);
- if (protoMan.TryIndex(DamageType, out var slashProto))
- damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
+ var slashProto = protoMan.Index(DamageType);
+ damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
});
// Check that running the suicide command kills the player
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/EdgeClobbering.cs b/Content.IntegrationTests/Tests/Construction/Interaction/EdgeClobbering.cs
new file mode 100644
index 000000000000..9f578148cfb5
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/EdgeClobbering.cs
@@ -0,0 +1,49 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Server.Construction.Components;
+using Content.Shared.Temperature;
+
+namespace Content.IntegrationTests.Tests.Construction.Interaction;
+
+public sealed class EdgeClobbering : InteractionTest
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: constructionGraph
+ id: ExampleGraph
+ start: A
+ graph:
+ - node: A
+ edges:
+ - to: B
+ steps:
+ - tool: Anchoring
+ doAfter: 1
+ - to: C
+ steps:
+ - tool: Screwing
+ doAfter: 1
+ - node: B
+ - node: C
+
+- type: entity
+ id: ExampleEntity
+ components:
+ - type: Construction
+ graph: ExampleGraph
+ node: A
+
+ ";
+
+ [Test]
+ public async Task EnsureNoEdgeClobbering()
+ {
+ await SpawnTarget("ExampleEntity");
+ var sTarget = SEntMan.GetEntity(Target!.Value);
+
+ await InteractUsing(Screw, false);
+ SEntMan.EventBus.RaiseLocalEvent(sTarget, new OnTemperatureChangeEvent(0f, 0f, 0f));
+ await AwaitDoAfters();
+
+ Assert.That(SEntMan.GetComponent(sTarget).Node, Is.EqualTo("C"));
+ }
+}
diff --git a/Content.IntegrationTests/Tests/ContrabandTest.cs b/Content.IntegrationTests/Tests/ContrabandTest.cs
index a33e7c2067bc..c52ef293e1eb 100644
--- a/Content.IntegrationTests/Tests/ContrabandTest.cs
+++ b/Content.IntegrationTests/Tests/ContrabandTest.cs
@@ -27,8 +27,11 @@ await client.WaitAssertion(() =>
if (!proto.TryGetComponent(out var contraband, componentFactory))
continue;
- Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
- @$"{proto.ID} has a ContrabandComponent with a unknown severity.");
+ if (!protoMan.TryIndex(contraband.Severity, out var severity))
+ {
+ Assert.Fail($"{proto.ID} has a ContrabandComponent with a unknown severity.");
+ continue;
+ }
if (!severity.ShowDepartmentsAndJobs)
continue;
diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitInteractionTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitInteractionTest.cs
new file mode 100644
index 000000000000..d4b20a55a861
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitInteractionTest.cs
@@ -0,0 +1,54 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Server.Containers;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Disposal;
+
+public sealed class DisposalUnitInteractionTest : InteractionTest
+{
+ private static readonly EntProtoId DisposalUnit = "DisposalUnit";
+ private static readonly EntProtoId TrashItem = "BrokenBottle";
+
+ private const string TestDisposalUnitId = "TestDisposalUnit";
+
+ [TestPrototypes]
+ private static readonly string TestPrototypes = $@"
+# A modified disposal unit with a 100% chance of a thrown item being inserted
+- type: entity
+ parent: {DisposalUnit.Id}
+ id: {TestDisposalUnitId}
+ components:
+ - type: ThrowInsertContainer
+ probability: 1
+";
+
+ ///
+ /// Spawns a disposal unit, gives the player a trash item, and makes the
+ /// player throw the item at the disposal unit.
+ /// After a short delay, verifies that the thrown item is contained inside
+ /// the disposal unit.
+ ///
+ [Test]
+ public async Task ThrowItemIntoDisposalUnitTest()
+ {
+ var containerSys = Server.System();
+
+ // Spawn the target disposal unit
+ var disposalUnit = await SpawnTarget(TestDisposalUnitId);
+
+ // Give the player some trash to throw
+ var trash = await PlaceInHands(TrashItem);
+
+ // Throw the item at the disposal unit
+ await ThrowItem();
+
+ // Wait a moment
+ await RunTicks(10);
+
+ // Make sure the trash is in the disposal unit
+ var throwInsertComp = Comp();
+ var container = containerSys.GetContainer(ToServer(disposalUnit), throwInsertComp.ContainerId);
+ Assert.That(container.ContainedEntities, Contains.Item(ToServer(trash)));
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
index e47c73611a41..69fe66039b8c 100644
--- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
+++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
@@ -21,6 +21,7 @@ public sealed class AirlockTest
components:
- type: Physics
bodyType: Dynamic
+ - type: GravityAffected
- type: Fixtures
fixtures:
fix1:
diff --git a/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs b/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs
new file mode 100644
index 000000000000..a7203d9259da
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs
@@ -0,0 +1,20 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Engineering.Systems;
+
+namespace Content.IntegrationTests.Tests.Engineering;
+
+[TestFixture]
+[TestOf(typeof(InflatableSafeDisassemblySystem))]
+public sealed class InflatablesDeflateTest : InteractionTest
+{
+ [Test]
+ public async Task Test()
+ {
+ await SpawnTarget(InflatableWall);
+
+ await InteractUsing(Needle);
+
+ AssertDeleted();
+ await AssertEntityLookup(new EntitySpecifier(InflatableWallStack.Id, 1));
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
index 6aa2763888d4..0951e7e26087 100644
--- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
+++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
@@ -19,6 +19,7 @@ public sealed class WeightlessStatusTests
- type: Alerts
- type: Physics
bodyType: Dynamic
+ - type: GravityAffected
- type: entity
name: WeightlessGravityGeneratorDummy
diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs
index b32d6c2b8d8c..8257035de66c 100644
--- a/Content.IntegrationTests/Tests/GravityGridTest.cs
+++ b/Content.IntegrationTests/Tests/GravityGridTest.cs
@@ -76,8 +76,8 @@ await server.WaitAssertion(() =>
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.True);
- Assert.That(!entityMan.GetComponent(grid1).EnabledVV);
- Assert.That(entityMan.GetComponent(grid2).EnabledVV);
+ Assert.That(!entityMan.GetComponent(grid1).Enabled);
+ Assert.That(entityMan.GetComponent(grid2).Enabled);
});
// Re-enable needs power so it turns off again.
@@ -94,7 +94,7 @@ await server.WaitAssertion(() =>
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.False);
- Assert.That(entityMan.GetComponent(grid2).EnabledVV, Is.False);
+ Assert.That(entityMan.GetComponent(grid2).Enabled, Is.False);
});
});
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
index 5db5d91d0d43..8917ba7ead55 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Stacks;
+using Robust.Shared.Prototypes;
+
namespace Content.IntegrationTests.Tests.Interaction;
// This partial class contains various constant prototype IDs common to interaction tests.
@@ -32,4 +35,9 @@ public abstract partial class InteractionTest
protected const string Manipulator1 = "MicroManipulatorStockPart";
protected const string Battery1 = "PowerCellSmall";
protected const string Battery4 = "PowerCellHyper";
+
+ // Inflatables & Needle used to pop them
+ protected static readonly EntProtoId InflatableWall = "InflatableWall";
+ protected static readonly EntProtoId Needle = "WeaponMeleeNeedle";
+ protected static readonly ProtoId InflatableWallStack = "InflatableWall";
}
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index 3302b1bafc53..8a5859fe0608 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -264,9 +264,10 @@ await Server.WaitPost(() =>
/// The entity or stack prototype to spawn and place into the users hand
/// The number of entities to spawn. If the prototype is a stack, this sets the stack count.
/// Whether or not to wait for any do-afters to complete
- protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
+ /// If true, perform an alternate interaction instead of a standard one.
+ protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true, bool altInteract = false)
{
- await InteractUsing((id, quantity), awaitDoAfters);
+ await InteractUsing((id, quantity), awaitDoAfters, altInteract);
}
///
@@ -274,7 +275,8 @@ protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfte
///
/// The entity type & quantity to spawn and place into the users hand
/// Whether or not to wait for any do-afters to complete
- protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
+ /// If true, perform an alternate interaction instead of a standard one.
+ protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true, bool altInteract = false)
{
// For every interaction, we will also examine the entity, just in case this breaks something, somehow.
// (e.g., servers attempt to assemble construction examine hints).
@@ -284,18 +286,19 @@ protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters =
}
await PlaceInHands(entity);
- await Interact(awaitDoAfters);
+ await Interact(awaitDoAfters, altInteract);
}
///
/// Interact with an entity using the currently held entity.
///
/// Whether or not to wait for any do-afters to complete
- protected async Task Interact(bool awaitDoAfters = true)
+ /// If true, performs an alternate interaction instead of a standard one.
+ protected async Task Interact(bool awaitDoAfters = true, bool altInteract = false)
{
if (Target == null || !Target.Value.IsClientSide())
{
- await Interact(Target, TargetCoords, awaitDoAfters);
+ await Interact(Target, TargetCoords, awaitDoAfters, altInteract);
return;
}
@@ -311,23 +314,23 @@ protected async Task Interact(bool awaitDoAfters = true)
await CheckTargetChange();
}
- ///
- protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
+ ///
+ protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
var coords = SEntMan.GetCoordinates(coordinates);
Assert.That(coords.IsValid(SEntMan));
- await Interact(sTarget, coords, awaitDoAfters);
+ await Interact(sTarget, coords, awaitDoAfters, altInteract);
}
///
/// Interact with an entity using the currently held entity.
///
- protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
+ protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(Player, out var player));
- await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
+ await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target, altInteract: altInteract));
await RunTicks(1);
if (awaitDoAfters)
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 79756ea5b419..0ed42d34764f 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -144,6 +144,7 @@ public abstract partial class InteractionTest
- type: Stripping
- type: Puller
- type: Physics
+ - type: GravityAffected
- type: Tag
tags:
- CanPilot
diff --git a/Content.IntegrationTests/Tests/Lathe/LatheTest.cs b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs
index 2fe347f63623..c335f8d6c8a8 100644
--- a/Content.IntegrationTests/Tests/Lathe/LatheTest.cs
+++ b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs
@@ -88,14 +88,18 @@ await server.WaitAssertion(() =>
// Check each recipe assigned to this lathe
foreach (var recipeId in recipes)
{
- Assert.That(protoMan.TryIndex(recipeId, out var recipeProto));
+ if (!protoMan.TryIndex(recipeId, out var recipeProto))
+ {
+ Assert.Fail($"Lathe recipe '{recipeId}' does not exist");
+ continue;
+ }
// Track the total material volume of the recipe
var totalQuantity = 0;
// Check each material called for by the recipe
foreach (var (materialId, quantity) in recipeProto.Materials)
{
- Assert.That(protoMan.TryIndex(materialId, out var materialProto));
+ Assert.That(protoMan.HasIndex(materialId), $"Material '{materialId}' does not exist");
// Make sure the material is accepted by the lathe
Assert.That(acceptedMaterials, Does.Contain(materialId), $"Lathe {latheProto.ID} has recipe {recipeId} but does not accept any materials containing {materialId}");
totalQuantity += quantity;
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs
index 2f7751982938..1bda6fd4dba8 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs
@@ -145,10 +145,7 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
var damageable = entMan.GetComponent(entity);
- if (!protoMan.TryIndex(BluntDamageType, out var prototype))
- {
- return;
- }
+ var prototype = protoMan.Index(BluntDamageType);
damageableSystem.SetDamage(entity, damageable, new DamageSpecifier(prototype, FixedPoint2.New(401)));
Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mindId));
diff --git a/Content.IntegrationTests/Tests/Nutrition/WaterCoolerInteractionTest.cs b/Content.IntegrationTests/Tests/Nutrition/WaterCoolerInteractionTest.cs
new file mode 100644
index 000000000000..c15de639de49
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Nutrition/WaterCoolerInteractionTest.cs
@@ -0,0 +1,99 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+using Content.Shared.Storage.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Nutrition;
+
+public sealed class WaterCoolerInteractionTest : InteractionTest
+{
+ ///
+ /// ProtoId of the water cooler entity.
+ ///
+ private static readonly EntProtoId WaterCooler = "WaterCooler";
+
+ ///
+ /// ProtoId of the paper cup entity dispensed by the water cooler.
+ ///
+ private static readonly EntProtoId PaperCup = "DrinkWaterCup";
+
+ ///
+ /// ProtoId of the water reagent that is stored in the water cooler.
+ ///
+ private static readonly ProtoId Water = "Water";
+
+ ///
+ /// Spawns a water cooler and tests that the player can retrieve a paper cup
+ /// by interacting with it, and can return the paper cup by alt-interacting with it.
+ ///
+ [Test]
+ public async Task GetAndReturnCup()
+ {
+ // Spawn the water cooler
+ var cooler = await SpawnTarget(WaterCooler);
+
+ // Record how many paper cups are in the cooler
+ var binComp = Comp(cooler);
+ var initialCount = binComp.Items.Count;
+ Assert.That(binComp.Items, Is.Not.Empty, "Water cooler didn't start with any cups");
+
+ // Interact with the water cooler using an empty hand to grab a paper cup
+ await Interact();
+
+ var cup = HandSys.GetActiveItem((SPlayer, Hands));
+
+ Assert.Multiple(() =>
+ {
+ // Make sure the player is now holding a cup
+ Assert.That(cup, Is.Not.Null, "Player's hand is empty");
+ AssertPrototype(PaperCup, SEntMan.GetNetEntity(cup));
+
+ // Make sure the number of cups in the cooler has decreased by one
+ Assert.That(binComp.Items, Has.Count.EqualTo(initialCount - 1), "Number of cups in cooler bin did not decrease by one");
+
+ // Make sure the cup isn't somehow still in the cooler too
+ Assert.That(binComp.Items, Does.Not.Contain(cup));
+ });
+
+ // Alt-interact with the water cooler while holding the cup to put it back
+ await Interact(altInteract: true);
+
+ Assert.Multiple(() =>
+ {
+ // Make sure the player's hand is empty
+ Assert.That(HandSys.ActiveHandIsEmpty((SPlayer, Hands)), "Player's hand is not empty");
+
+ // Make sure the count has gone back up by one
+ Assert.That(binComp.Items, Has.Count.EqualTo(initialCount), "Number of cups in cooler bin did not return to initial count");
+
+ // Make sure the cup is in the cooler
+ Assert.That(binComp.Items, Contains.Item(cup), "Cup was not returned to cooler");
+ });
+ }
+
+ ///
+ /// Spawns a water cooler and gives the player an empty paper cup.
+ /// Tests that the player can put water into the cup by interacting
+ /// with the water cooler while holding the cup.
+ ///
+ [Test]
+ public async Task FillCup()
+ {
+ var solutionSys = Server.System();
+
+ // Spawn the water cooler
+ await SpawnTarget(WaterCooler);
+
+ // Give the player a cup
+ var cup = await PlaceInHands(PaperCup);
+
+ // Make the player interact with the water cooler using the held cup
+ await Interact();
+
+ // Make sure the cup now contains water
+ Assert.That(solutionSys.GetTotalPrototypeQuantity(ToServer(cup), Water), Is.GreaterThan(FixedPoint2.Zero),
+ "Cup does not contain any water");
+ }
+}
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index 9d8755f8319f..378d2cf27d5b 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.RegularExpressions;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
using Content.Server.Maps;
@@ -49,16 +50,38 @@ public sealed class PostMapInitTest
AdminTestArenaSystem.ArenaMapPath
};
+ ///
+ /// A dictionary linking maps to collections of entity prototype ids that should be exempt from "DoNotMap" restrictions.
+ ///
+ ///
+ /// This declares that the listed entity prototypes are allowed to be present on the map
+ /// despite being categorized as "DoNotMap", while any unlisted prototypes will still
+ /// cause the test to fail.
+ ///
+ private static readonly Dictionary> DoNotMapWhitelistSpecific = new()
+ {
+ {"/Maps/bagel.yml", ["RubberStampMime"]},
+ {"/Maps/reach.yml", ["HandheldCrewMonitor"]},
+ {"/Maps/Shuttles/ShuttleEvent/honki.yml", ["GoldenBikeHorn", "RubberStampClown"]},
+ {"/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", ["RubberStampSyndicate"]},
+ {"/Maps/Shuttles/ShuttleEvent/cruiser.yml", ["ShuttleGunPerforator"]},
+ {"/Maps/Shuttles/ShuttleEvent/instigator.yml", ["ShuttleGunFriendship"]},
+ };
+
+ ///
+ /// Maps listed here are given blanket freedom to contain "DoNotMap" entities. Use sparingly.
+ ///
+ ///
+ /// It is also possible to whitelist entire directories here. For example, adding
+ /// "/Maps/Shuttles/**" will whitelist all shuttle maps.
+ ///
private static readonly string[] DoNotMapWhitelist =
{
"/Maps/centcomm.yml",
+ "/Maps/Shuttles/AdminSpawn/**", // admin gaming
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
"/Maps/reach.yml", // Contains handheld crew monitor
- "/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
- "/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp
- "/Maps/Shuttles/ShuttleEvent/instigator.yml", // Contains EXP-320g "Friendship"
- "/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", // Contains syndicate rubber stamp
- #region starlight
+ #region starlight
"/Maps/nanoStation.yml",
"/Maps/_Starlight/nukieplanet.yml", //starlight nukie spawn map
"/Maps/_Starlight/Centcomms/CC_Outpost_G24.yml", //starlight centcomm map
@@ -79,6 +102,13 @@ public sealed class PostMapInitTest
#endregion
};
+ ///
+ /// Converts the above globs into regex so your eyes dont bleed trying to add filepaths.
+ ///
+ private static readonly Regex[] DoNotMapWhiteListRegexes = DoNotMapWhitelist
+ .Select(glob => new Regex(GlobToRegex(glob), RegexOptions.IgnoreCase | RegexOptions.Compiled))
+ .ToArray();
+
private static readonly string[] GameMaps =
{
"Dev",
@@ -320,18 +350,30 @@ public async Task NoSavedPostMapInitTest()
await pair.CleanReturnAsync();
}
+ private bool IsWhitelistedForMap(EntProtoId protoId, ResPath map)
+ {
+ if (!DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var allowedProtos))
+ return false;
+
+ return allowedProtos.Contains(protoId);
+ }
+
///
/// Check that maps do not have any entities that belong to the DoNotMap entity category
///
private void CheckDoNotMap(ResPath map, YamlNode node, IPrototypeManager protoManager)
{
- if (DoNotMapWhitelist.Contains(map.ToString()))
- return;
+ foreach (var regex in DoNotMapWhiteListRegexes)
+ {
+ if (regex.IsMatch(map.ToString()))
+ return;
+ }
var yamlEntities = node["entities"];
- if (!protoManager.TryIndex(DoNotMapCategory, out var dnmCategory))
- return;
+ var dnmCategory = protoManager.Index(DoNotMapCategory);
+ // Make a set containing all the specific whitelisted proto ids for this map
+ HashSet unusedExemptions = DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var exemptions) ? new(exemptions) : [];
Assert.Multiple(() =>
{
foreach (var yamlEntity in (YamlSequenceNode)yamlEntities)
@@ -339,13 +381,20 @@ private void CheckDoNotMap(ResPath map, YamlNode node, IPrototypeManager protoMa
var protoId = yamlEntity["proto"].AsString();
// This doesn't properly handle prototype migrations, but thats not a significant issue.
- if (!protoManager.TryIndex(protoId, out var proto, false))
+ if (!protoManager.TryIndex(protoId, out var proto))
continue;
- Assert.That(!proto.Categories.Contains(dnmCategory),
+ Assert.That(!proto.Categories.Contains(dnmCategory) || IsWhitelistedForMap(protoId, map),
$"\nMap {map} contains entities in the DO NOT MAP category ({proto.Name})");
+
+ // The proto id is used on this map, so remove it from the set
+ unusedExemptions.Remove(protoId);
}
});
+
+ // If there are any proto ids left, they must not have been used in the map!
+ Assert.That(unusedExemptions, Is.Empty,
+ $"Map {map} has DO NOT MAP entities whitelisted that are not present in the map: {string.Join(", ", unusedExemptions)}");
}
private bool IsPreInit(ResPath map,
@@ -642,5 +691,20 @@ await server.WaitPost(() =>
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
+
+ ///
+ /// Lets us the convert the filepaths to regex without eyeglaze trying to add new paths.
+ ///
+ private static string GlobToRegex(string glob)
+ {
+ var regex = Regex.Escape(glob)
+ .Replace(@"\*\*", "**") // replace **
+ .Replace(@"\*", "*") // replace *
+ .Replace("**", ".*") // ** → match across folders
+ .Replace("*", @"[^/]*") // * → match within a single folder
+ .Replace(@"\?", "."); // ? → any single character
+
+ return $"^{regex}$";
+ }
}
}
diff --git a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
index ecb11fc1ba4f..ca70120ee9a7 100644
--- a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
+++ b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs
@@ -25,6 +25,11 @@ await Server.WaitAssertion(() =>
if (ignored.Contains(cmd.Cmd.GetType().Assembly))
continue;
+ // Only care about content commands.
+ var assemblyName = cmd.Cmd.GetType().Assembly.FullName;
+ if (assemblyName == null || !assemblyName.StartsWith("Content."))
+ continue;
+
Assert.That(admin.TryGetCommandFlags(cmd, out _), $"Command does not have admin permissions set up: {cmd.FullName()}");
}
});
diff --git a/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs b/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs
new file mode 100644
index 000000000000..c9b781ddf03d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs
@@ -0,0 +1,42 @@
+using Content.Shared.Kitchen;
+
+namespace Content.IntegrationTests.Tests.WizdenContentFreeze;
+
+///
+/// These tests are limited to adding a specific type of content, essentially freezing it. If you are a fork developer, you may want to disable these tests.
+///
+public sealed class WizdenContentFreeze
+{
+ ///
+ /// This freeze prohibits the addition of new microwave recipes.
+ /// The maintainers decided that the mechanics of cooking food in the microwave should be removed,
+ /// and all recipes should be ported to other cooking methods.
+ /// All added recipes essentially increase the technical debt of future cooking refactoring.
+ ///
+ /// https://github.com/space-wizards/space-station-14/issues/8524
+ ///
+ [Test]
+ [Explicit] //Starlight: We dont give a fuck about your microwave recipe freeze
+ public async Task MicrowaveRecipesFreezeTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var protoMan = server.ProtoMan;
+
+ var recipesCount = protoMan.Count();
+ var recipesLimit = 218;
+
+ if (recipesCount > recipesLimit)
+ {
+ Assert.Fail($"PLEASE STOP ADDING NEW MICROWAVE RECIPES. MICROWAVE RECIPES ARE FROZEN AND NEED TO BE REPLACED WITH PROPER COOKING MECHANICS! See https://github.com/space-wizards/space-station-14/issues/8524. Keep it under {recipesLimit}. Current count: {recipesCount}");
+ }
+
+ if (recipesCount < recipesLimit)
+ {
+ Assert.Fail($"Oh, you deleted the microwave recipes? YOU ARE SO COOL! Please lower the number of recipes in MicrowaveRecipesFreezeTest from {recipesLimit} to {recipesCount} so that future contributors cannot add new recipes back.");
+ }
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/_Starlight/Thaven/MoodTests.cs b/Content.IntegrationTests/Tests/_Starlight/Thaven/MoodTests.cs
new file mode 100644
index 000000000000..5b51cc1549ae
--- /dev/null
+++ b/Content.IntegrationTests/Tests/_Starlight/Thaven/MoodTests.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Content.IntegrationTests;
+using Content.Server._Starlight.Thaven;
+using Content.Shared.Dataset;
+using Content.Shared._Starlight.Thaven;
+using NUnit.Framework;
+using Robust.Shared.ContentPack;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+
+namespace Content.IntegrationTests.Tests._Starlight.Thaven;
+
+[TestFixture, TestOf(typeof(ThavenMoodPrototype))]
+public sealed class ThavenMoodTests
+{
+ [TestPrototypes]
+ const string PROTOTYPES = @"
+- type: dataset
+ id: ThreeValueSet
+ values:
+ - One
+ - Two
+ - Three
+- type: thavenMood
+ id: DuplicateTest
+ moodName: DuplicateTest
+ moodDesc: DuplicateTest
+ allowDuplicateMoodVars: false
+ moodVars:
+ a: ThreeValueSet
+ b: ThreeValueSet
+ c: ThreeValueSet
+- type: thavenMood
+ id: DuplicateOverlapTest
+ moodName: DuplicateOverlapTest
+ moodDesc: DuplicateOverlapTest
+ allowDuplicateMoodVars: false
+ moodVars:
+ a: ThreeValueSet
+ b: ThreeValueSet
+ c: ThreeValueSet
+ d: ThreeValueSet
+ e: ThreeValueSet
+";
+
+ [Test]
+ [Repeat(10)]
+ public async Task TestDuplicatePrevention()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+ await server.WaitIdleAsync();
+
+ var entMan = server.ResolveDependency();
+ var thavenSystem = entMan.System();
+ var protoMan = server.ResolveDependency();
+
+ var dataset = protoMan.Index("ThreeValueSet");
+ var moodProto = protoMan.Index("DuplicateTest");
+
+ var datasetSet = dataset.Values.ToHashSet();
+ var mood = thavenSystem.RollMood(moodProto);
+ var moodVarSet = mood.MoodVars.Values.ToHashSet();
+
+ Assert.That(moodVarSet, Is.EquivalentTo(datasetSet));
+
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ [Repeat(10)]
+ public async Task TestDuplicateOverlap()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var thavenSystem = entMan.System();
+ var protoMan = server.ResolveDependency();
+
+ var dataset = protoMan.Index("ThreeValueSet");
+ var moodProto = protoMan.Index("DuplicateOverlapTest");
+
+ var datasetSet = dataset.Values.ToHashSet();
+ var mood = thavenSystem.RollMood(moodProto);
+ var moodVarSet = mood.MoodVars.Values.ToHashSet();
+
+ Assert.That(moodVarSet, Is.EquivalentTo(datasetSet));
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.Packaging/ClientPackaging.cs b/Content.Packaging/ClientPackaging.cs
index 6d0a46279076..21215d3bcbfc 100644
--- a/Content.Packaging/ClientPackaging.cs
+++ b/Content.Packaging/ClientPackaging.cs
@@ -78,7 +78,11 @@ await RobustSharedPackaging.WriteContentAssemblies(
new[] { "Content.Client", "Content.Shared", "Content.Shared.Database" },
cancel: cancel);
- await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);
+ await RobustClientPackaging.WriteClientResources(
+ contentDir,
+ inputPass,
+ SharedPackaging.AdditionalIgnoredResources,
+ cancel);
inputPass.InjectFinished();
}
diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs
index 283878b325da..153cec976e9b 100644
--- a/Content.Packaging/ServerPackaging.cs
+++ b/Content.Packaging/ServerPackaging.cs
@@ -25,6 +25,12 @@ public static class ServerPackaging
new PlatformReg("freebsd-x64", "FreeBSD", false),
};
+ private static IReadOnlySet ServerContentIgnoresResources { get; } = new HashSet
+ {
+ "ServerInfo",
+ "Changelog",
+ };
+
private static List PlatformRids => Platforms
.Select(o => o.Rid)
.ToList();
@@ -222,7 +228,11 @@ await RobustSharedPackaging.WriteContentAssemblies(
contentAssemblies,
cancel: cancel);
- await RobustServerPackaging.WriteServerResources(contentDir, inputPassResources, cancel);
+ await RobustServerPackaging.WriteServerResources(
+ contentDir,
+ inputPassResources,
+ ServerContentIgnoresResources.Concat(SharedPackaging.AdditionalIgnoredResources).ToHashSet(),
+ cancel);
if (hybridAcz)
{
diff --git a/Content.Packaging/SharedPackaging.cs b/Content.Packaging/SharedPackaging.cs
new file mode 100644
index 000000000000..5888845588da
--- /dev/null
+++ b/Content.Packaging/SharedPackaging.cs
@@ -0,0 +1,10 @@
+namespace Content.Packaging;
+
+public sealed class SharedPackaging
+{
+ public static readonly IReadOnlySet AdditionalIgnoredResources = new HashSet
+ {
+ // MapRenderer outputs into Resources. Avoid these getting included in packaging.
+ "MapImages",
+ };
+}
diff --git a/Content.Server/Access/Systems/AgentIDCardSystem.cs b/Content.Server/Access/Systems/AgentIDCardSystem.cs
index 0df760baef1d..1706908e72d9 100644
--- a/Content.Server/Access/Systems/AgentIDCardSystem.cs
+++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs
@@ -45,7 +45,7 @@ private void OnChameleonControllerOutfitChangedItem(Entity
if (!TryComp(ent, out var idCardComp))
return;
- _prototypeManager.TryIndex(args.Args.ChameleonOutfit.Job, out var jobProto);
+ _prototypeManager.Resolve(args.Args.ChameleonOutfit.Job, out var jobProto);
var jobIcon = args.Args.ChameleonOutfit.Icon ?? jobProto?.Icon;
var jobName = args.Args.ChameleonOutfit.Name ?? jobProto?.Name ?? "";
@@ -130,7 +130,7 @@ private void OnJobIconChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDC
if (!TryComp(uid, out var idCard))
return;
- if (!_prototypeManager.TryIndex(args.JobIconId, out var jobIcon))
+ if (!_prototypeManager.Resolve(args.JobIconId, out var jobIcon))
return;
_cardSystem.TryChangeJobIcon(uid, jobIcon, idCard);
diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
index 0375ced02348..731566cd77a9 100644
--- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs
+++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
@@ -99,7 +99,7 @@ private void UpdateUserInterface(EntityUid uid, IdCardConsoleComponent component
var targetIdComponent = Comp(targetId);
var targetAccessComponent = Comp(targetId);
- var jobProto = targetIdComponent.JobPrototype ?? new ProtoId(string.Empty);
+ var jobProto = targetIdComponent.JobPrototype ?? new ProtoId(string.Empty);
if (TryComp(targetId, out var keyStorage)
&& keyStorage.Key is { } key
&& _record.TryGetRecord(key, out var record))
@@ -131,7 +131,7 @@ private void TryWriteToTargetId(EntityUid uid,
string newFullName,
string newJobTitle,
List> newAccessList,
- ProtoId newJobProto,
+ ProtoId newJobProto,
EntityUid player,
IdCardConsoleComponent? component = null)
{
@@ -145,7 +145,7 @@ private void TryWriteToTargetId(EntityUid uid,
_idCard.TryChangeJobTitle(targetId, newJobTitle, player: player);
if (_prototype.TryIndex(newJobProto, out var job)
- && _prototype.TryIndex(job.Icon, out var jobIcon))
+ && _prototype.Resolve(job.Icon, out var jobIcon))
{
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
_idCard.TryChangeJobDepartment(targetId, job);
diff --git a/Content.Server/Access/Systems/PresetIdCardSystem.cs b/Content.Server/Access/Systems/PresetIdCardSystem.cs
index 426e523243c3..6d9f61d94118 100644
--- a/Content.Server/Access/Systems/PresetIdCardSystem.cs
+++ b/Content.Server/Access/Systems/PresetIdCardSystem.cs
@@ -82,7 +82,7 @@ private void SetupIdAccess(EntityUid uid, PresetIdCardComponent id, bool extende
_cardSystem.TryChangeJobTitle(uid, job.LocalizedName);
_cardSystem.TryChangeJobDepartment(uid, job);
- if (_prototypeManager.TryIndex(job.Icon, out var jobIcon))
+ if (_prototypeManager.Resolve(job.Icon, out var jobIcon))
_cardSystem.TryChangeJobIcon(uid, jobIcon);
}
}
diff --git a/Content.Server/Administration/Commands/AddEntityStorageCommand.cs b/Content.Server/Administration/Commands/AddEntityStorageCommand.cs
index 4c562d606d44..b7b6ad89e921 100644
--- a/Content.Server/Administration/Commands/AddEntityStorageCommand.cs
+++ b/Content.Server/Administration/Commands/AddEntityStorageCommand.cs
@@ -1,4 +1,4 @@
-using Content.Server.Storage.Components;
+using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;
diff --git a/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs b/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs
index b92cbfc0de97..f089268beae1 100644
--- a/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs
+++ b/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs
@@ -6,17 +6,13 @@
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
-public sealed class AddPolymorphActionCommand : IConsoleCommand
+public sealed class AddPolymorphActionCommand : LocalizedEntityCommands
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly PolymorphSystem _polySystem = default!;
- public string Command => "addpolymorphaction";
+ public override string Command => "addpolymorphaction";
- public string Description => Loc.GetString("add-polymorph-action-command-description");
-
- public string Help => Loc.GetString("add-polymorph-action-command-help-text");
-
- public void Execute(IConsoleShell shell, string argStr, string[] args)
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
@@ -24,15 +20,13 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}
- if (!NetEntity.TryParse(args[0], out var entityUidNet) || !_entityManager.TryGetEntity(entityUidNet, out var entityUid))
+ if (!NetEntity.TryParse(args[0], out var entityUidNet) || !EntityManager.TryGetEntity(entityUidNet, out var entityUid))
{
- shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
+ shell.WriteError(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
return;
}
- var polySystem = _entityManager.EntitySysManager.GetEntitySystem();
-
- var polymorphable = _entityManager.EnsureComponent(entityUid.Value);
- polySystem.CreatePolymorphAction(args[1], (entityUid.Value, polymorphable));
+ var polymorphable = EntityManager.EnsureComponent(entityUid.Value);
+ _polySystem.CreatePolymorphAction(args[1], (entityUid.Value, polymorphable));
}
}
diff --git a/Content.Server/Administration/Commands/ExplosionCommand.cs b/Content.Server/Administration/Commands/ExplosionCommand.cs
index 787886f164a3..6f9e89243d7c 100644
--- a/Content.Server/Administration/Commands/ExplosionCommand.cs
+++ b/Content.Server/Administration/Commands/ExplosionCommand.cs
@@ -118,7 +118,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}
}
- else if (!_prototypeManager.TryIndex(ExplosionSystem.DefaultExplosionPrototypeId, out type))
+ else if (!_prototypeManager.Resolve(ExplosionSystem.DefaultExplosionPrototypeId, out type))
{
// no prototype was specified, so lets default to whichever one was defined first
type = _prototypeManager.EnumeratePrototypes().FirstOrDefault();
diff --git a/Content.Server/Administration/Commands/LinkBluespaceLocker.cs b/Content.Server/Administration/Commands/LinkBluespaceLocker.cs
index 9fac72664b63..47d87565d2a6 100644
--- a/Content.Server/Administration/Commands/LinkBluespaceLocker.cs
+++ b/Content.Server/Administration/Commands/LinkBluespaceLocker.cs
@@ -1,5 +1,6 @@
using Content.Server.Storage.Components;
using Content.Shared.Administration;
+using Content.Shared.Storage.Components;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
diff --git a/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs b/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs
index 858e48a71e65..f267cd28cb1a 100644
--- a/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs
+++ b/Content.Server/Administration/Commands/RemoveEntityStorageCommand.cs
@@ -1,4 +1,4 @@
-using Content.Server.Storage.Components;
+using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;
diff --git a/Content.Server/Administration/Logs/AdminLogManager.Json.cs b/Content.Server/Administration/Logs/AdminLogManager.Json.cs
index 9e6274a49330..a0a3b920bdfa 100644
--- a/Content.Server/Administration/Logs/AdminLogManager.Json.cs
+++ b/Content.Server/Administration/Logs/AdminLogManager.Json.cs
@@ -2,9 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Administration.Logs.Converters;
-using Robust.Server.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
+using Robust.Shared.Collections;
namespace Content.Server.Administration.Logs;
@@ -22,55 +20,25 @@ private void InitializeJson()
PropertyNamingPolicy = NamingPolicy
};
+ var interfaces = new ValueList();
+
foreach (var converter in _reflection.FindTypesWithAttribute())
{
var instance = _typeFactory.CreateInstance(converter);
- (instance as IAdminLogConverter)?.Init(_dependencies);
+ if (instance is IAdminLogConverter converterInterface)
+ {
+ interfaces.Add(converterInterface);
+ converterInterface.Init(_dependencies);
+ }
_jsonOptions.Converters.Add(instance);
}
- var converterNames = _jsonOptions.Converters.Select(converter => converter.GetType().Name);
- _sawmill.Debug($"Admin log converters found: {string.Join(" ", converterNames)}");
- }
-
- private (JsonDocument Json, HashSet Players) ToJson(
- Dictionary properties)
- {
- var players = new HashSet();
- var parsed = new Dictionary();
-
- foreach (var key in properties.Keys)
+ foreach (var @interface in interfaces)
{
- var value = properties[key];
- value = value switch
- {
- ICommonSession player => new SerializablePlayer(player),
- EntityCoordinates entityCoordinates => new SerializableEntityCoordinates(_entityManager, entityCoordinates),
- _ => value
- };
-
- var parsedKey = NamingPolicy.ConvertName(key);
- parsed.Add(parsedKey, value);
-
- var entityId = properties[key] switch
- {
- EntityUid id => id,
- EntityStringRepresentation rep => rep.Uid,
- ICommonSession {AttachedEntity: {Valid: true}} session => session.AttachedEntity,
- IComponent component => component.Owner,
- _ => null
- };
-
- if (_entityManager.TryGetComponent(entityId, out ActorComponent? actor))
- {
- players.Add(actor.PlayerSession.UserId.UserId);
- }
- else if (value is SerializablePlayer player)
- {
- players.Add(player.Player.UserId.UserId);
- }
+ @interface.Init2(_jsonOptions);
}
- return (JsonSerializer.SerializeToDocument(parsed, _jsonOptions), players);
+ var converterNames = _jsonOptions.Converters.Select(converter => converter.GetType().Name);
+ _sawmill.Debug($"Admin log converters found: {string.Join(" ", converterNames)}");
}
}
diff --git a/Content.Server/Administration/Logs/AdminLogManager.cs b/Content.Server/Administration/Logs/AdminLogManager.cs
index 600311a65145..e7682cf5595b 100644
--- a/Content.Server/Administration/Logs/AdminLogManager.cs
+++ b/Content.Server/Administration/Logs/AdminLogManager.cs
@@ -25,7 +25,6 @@ namespace Content.Server.Administration.Logs;
public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogManager
{
[Dependency] private readonly IConfigurationManager _configuration = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -72,7 +71,6 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
// CVars
private bool _metricsEnabled;
- private bool _enabled;
private TimeSpan _queueSendDelay;
private int _queueMax;
private int _preRoundQueueMax;
@@ -103,7 +101,7 @@ public void Initialize()
_configuration.OnValueChanged(CVars.MetricsEnabled,
value => _metricsEnabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsEnabled,
- value => _enabled = value, true);
+ value => Enabled = value, true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueSendDelay,
value => _queueSendDelay = TimeSpan.FromSeconds(value), true);
_configuration.OnValueChanged(CCVars.AdminLogsQueueMax,
@@ -123,6 +121,12 @@ public void Initialize()
}
}
+ public override string ConvertName(string name)
+ {
+ // JsonNamingPolicy is not whitelisted by the sandbox.
+ return NamingPolicy.ConvertName(name);
+ }
+
public async Task Shutdown()
{
if (!_logQueue.IsEmpty)
@@ -292,8 +296,17 @@ public void RunLevelChanged(GameRunLevel level)
}
}
- private void Add(LogType type, LogImpact impact, string message, JsonDocument json, HashSet players)
+ public override void Add(LogType type, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] ref LogStringHandler handler)
{
+ Add(type, LogImpact.Medium, ref handler);
+ }
+
+ public override void Add(LogType type, LogImpact impact, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] ref LogStringHandler handler)
+ {
+ var message = handler.ToStringAndClear();
+ if (!Enabled)
+ return;
+
var preRound = _runLevel == GameRunLevel.PreRoundLobby;
var count = preRound ? _preRoundLogQueue.Count : _logQueue.Count;
if (count >= _dropThreshold)
@@ -302,6 +315,10 @@ private void Add(LogType type, LogImpact impact, string message, JsonDocument js
return;
}
+ var json = JsonSerializer.SerializeToDocument(handler.Values, _jsonOptions);
+ var id = NextLogId;
+ var players = GetPlayers(handler.Values, id);
+
// PostgreSQL does not support storing null chars in text values.
if (message.Contains('\0'))
{
@@ -311,31 +328,85 @@ private void Add(LogType type, LogImpact impact, string message, JsonDocument js
var log = new AdminLog
{
- Id = NextLogId,
+ Id = id,
RoundId = _currentRoundId,
Type = type,
Impact = impact,
Date = DateTime.UtcNow,
Message = message,
Json = json,
- Players = new List(players.Count)
+ Players = players,
};
- var adminLog = false;
- var adminSys = _entityManager.SystemOrNull();
- var logMessage = message;
+ DoAdminAlerts(players, message, impact);
+
+ if (preRound)
+ {
+ _preRoundLogQueue.Enqueue(log);
+ }
+ else
+ {
+ _logQueue.Enqueue(log);
+ CacheLog(log);
+ }
+ }
- foreach (var id in players)
+ private List GetPlayers(Dictionary values, int logId)
+ {
+ List players = new();
+ foreach (var value in values.Values)
{
- var player = new AdminLogPlayer
+ switch (value)
{
- LogId = log.Id,
- PlayerUserId = id
- };
+ case SerializablePlayer player:
+ AddPlayer(players, player.UserId, logId);
+ continue;
+
+ case EntityStringRepresentation rep:
+ if (rep.Session is {} session)
+ AddPlayer(players, session.UserId.UserId, logId);
+ continue;
+
+ case IAdminLogsPlayerValue playerValue:
+ foreach (var player in playerValue.Players)
+ {
+ AddPlayer(players, player, logId);
+ }
- log.Players.Add(player);
+ break;
+ }
+ }
- if (adminSys != null)
+ return players;
+ }
+
+ private void AddPlayer(List players, Guid user, int logId)
+ {
+ // The majority of logs have a single player, or maybe two. Instead of allocating a List and
+ // HashSet, we just iterate over the list to check for duplicates.
+ foreach (var player in players)
+ {
+ if (player.PlayerUserId == user)
+ return;
+ }
+
+ players.Add(new AdminLogPlayer
+ {
+ LogId = logId,
+ PlayerUserId = user
+ });
+ }
+
+ private void DoAdminAlerts(List players, string message, LogImpact impact)
+ {
+ var adminLog = false;
+ var logMessage = message;
+
+ foreach (var player in players)
+ {
+ var id = player.PlayerUserId;
+
+ if (EntityManager.TrySystem(out AdminSystem? adminSys))
{
var cachedInfo = adminSys.GetCachedPlayerInfo(new NetUserId(id));
if (cachedInfo != null && cachedInfo.Antag)
@@ -372,35 +443,6 @@ private void Add(LogType type, LogImpact impact, string message, JsonDocument js
if (adminLog)
_chat.SendAdminAlert(logMessage);
-
- if (preRound)
- {
- _preRoundLogQueue.Enqueue(log);
- }
- else
- {
- _logQueue.Enqueue(log);
- CacheLog(log);
- }
- }
-
- public override void Add(LogType type, LogImpact impact, ref LogStringHandler handler)
- {
- if (!_enabled)
- {
- handler.ToStringAndClear();
- return;
- }
-
- var (json, players) = ToJson(handler.Values);
- var message = handler.ToStringAndClear();
-
- Add(type, impact, message, json, players);
- }
-
- public override void Add(LogType type, ref LogStringHandler handler)
- {
- Add(type, LogImpact.Medium, ref handler);
}
public async Task> All(LogFilter? filter = null, Func>? listProvider = null)
diff --git a/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs b/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs
index 7eaab9ba2810..778f84c1acb5 100644
--- a/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/AdminLogConverter.cs
@@ -6,6 +6,13 @@ namespace Content.Server.Administration.Logs.Converters;
public interface IAdminLogConverter
{
void Init(IDependencyCollection dependencies);
+
+ ///
+ /// Called after all converters have been added to the .
+ ///
+ void Init2(JsonSerializerOptions options)
+ {
+ }
}
public abstract class AdminLogConverter : JsonConverter, IAdminLogConverter
@@ -14,6 +21,10 @@ public virtual void Init(IDependencyCollection dependencies)
{
}
+ public virtual void Init2(JsonSerializerOptions options)
+ {
+ }
+
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException();
diff --git a/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs b/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs
index fb5c6a6fe54d..3a0ffeb758ee 100644
--- a/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/EntityCoordinatesConverter.cs
@@ -6,7 +6,7 @@
namespace Content.Server.Administration.Logs.Converters;
[AdminLogConverter]
-public sealed class EntityCoordinatesConverter : AdminLogConverter
+public sealed class EntityCoordinatesConverter : AdminLogConverter
{
// System.Text.Json actually keeps hold of your JsonSerializerOption instances in a cache on .NET 7.
// Use a weak reference to avoid holding server instances live too long in integration tests.
@@ -17,15 +17,16 @@ public override void Init(IDependencyCollection dependencies)
_entityManager = new WeakReference(dependencies.Resolve());
}
- public void Write(Utf8JsonWriter writer, SerializableEntityCoordinates value, JsonSerializerOptions options, IEntityManager entities)
+ public void Write(Utf8JsonWriter writer, EntityCoordinates value, JsonSerializerOptions options, IEntityManager entities)
{
writer.WriteStartObject();
- WriteEntityInfo(writer, value.EntityUid, entities, "parent");
+ WriteEntityInfo(writer, value.EntityId, entities, "parent");
writer.WriteNumber("x", value.X);
writer.WriteNumber("y", value.Y);
- if (value.MapUid.HasValue)
+ var mapUid = value.GetMapUid(entities);
+ if (mapUid.HasValue)
{
- WriteEntityInfo(writer, value.MapUid.Value, entities, "map");
+ WriteEntityInfo(writer, mapUid.Value, entities, "map");
}
writer.WriteEndObject();
}
@@ -33,7 +34,7 @@ public void Write(Utf8JsonWriter writer, SerializableEntityCoordinates value, Js
private static void WriteEntityInfo(Utf8JsonWriter writer, EntityUid value, IEntityManager entities, string rootName)
{
writer.WriteStartObject(rootName);
- writer.WriteNumber("uid", value.GetHashCode());
+ writer.WriteNumber("uid", value.Id);
if (entities.TryGetComponent(value, out MetaDataComponent? metaData))
{
writer.WriteString("name", metaData.EntityName);
@@ -51,7 +52,7 @@ private static void WriteEntityInfo(Utf8JsonWriter writer, EntityUid value, IEnt
writer.WriteEndObject();
}
- public override void Write(Utf8JsonWriter writer, SerializableEntityCoordinates value, JsonSerializerOptions options)
+ public override void Write(Utf8JsonWriter writer, EntityCoordinates value, JsonSerializerOptions options)
{
if (!_entityManager.TryGetTarget(out var entityManager))
throw new InvalidOperationException("EntityManager got garbage collected!");
@@ -59,19 +60,3 @@ public override void Write(Utf8JsonWriter writer, SerializableEntityCoordinates
Write(writer, value, options, entityManager);
}
}
-
-public readonly struct SerializableEntityCoordinates
-{
- public readonly EntityUid EntityUid;
- public readonly float X;
- public readonly float Y;
- public readonly EntityUid? MapUid;
-
- public SerializableEntityCoordinates(IEntityManager entityManager, EntityCoordinates coordinates)
- {
- EntityUid = coordinates.EntityId;
- X = coordinates.X;
- Y = coordinates.Y;
- MapUid = entityManager.System().GetMap(coordinates);
- }
-}
diff --git a/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs b/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs
index 39d34e5f1833..9a92a2cb4666 100644
--- a/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/EntityStringRepresentationConverter.cs
@@ -1,6 +1,5 @@
using System.Text.Json;
using Content.Server.Administration.Managers;
-using Robust.Server.Player;
namespace Content.Server.Administration.Logs.Converters;
@@ -24,7 +23,7 @@ public override void Write(Utf8JsonWriter writer, EntityStringRepresentation val
{
writer.WriteString("player", value.Session.UserId.UserId);
- if (_adminManager.IsAdmin(value.Uid))
+ if (_adminManager.IsAdmin(value.Session))
{
writer.WriteBoolean("admin", true);
}
diff --git a/Content.Server/Administration/Logs/Converters/MindStringRepresentationConverter.cs b/Content.Server/Administration/Logs/Converters/MindStringRepresentationConverter.cs
new file mode 100644
index 000000000000..b43d2a90e7bd
--- /dev/null
+++ b/Content.Server/Administration/Logs/Converters/MindStringRepresentationConverter.cs
@@ -0,0 +1,38 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Content.Shared.Mind;
+
+namespace Content.Server.Administration.Logs.Converters;
+
+[AdminLogConverter]
+public sealed class MindStringRepresentationConverter : AdminLogConverter
+{
+ private JsonConverter _converter = null!;
+
+ public override void Init2(JsonSerializerOptions options)
+ {
+ base.Init2(options);
+
+ _converter = (JsonConverter)
+ options.GetConverter(typeof(EntityStringRepresentation));
+ }
+
+ public override void Write(Utf8JsonWriter writer, MindStringRepresentation value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+
+ if (value.OwnedEntity is { } owned)
+ {
+ writer.WritePropertyName("owned");
+ _converter.Write(writer, owned, options);
+ }
+
+ if (value.Player is { } player)
+ {
+ writer.WriteString("player", player);
+ writer.WriteBoolean("present", value.PlayerPresent);
+ }
+
+ writer.WriteEndObject();
+ }
+}
diff --git a/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs b/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs
index c1567448ccb8..d1a009b8cd91 100644
--- a/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs
+++ b/Content.Server/Administration/Logs/Converters/PlayerSessionConverter.cs
@@ -1,45 +1,23 @@
using System.Text.Json;
-using Robust.Shared.Player;
+using Content.Shared.Administration.Logs;
namespace Content.Server.Administration.Logs.Converters;
[AdminLogConverter]
public sealed class PlayerSessionConverter : AdminLogConverter
{
- // System.Text.Json actually keeps hold of your JsonSerializerOption instances in a cache on .NET 7.
- // Use a weak reference to avoid holding server instances live too long in integration tests.
- private WeakReference _entityManager = default!;
-
- public override void Init(IDependencyCollection dependencies)
- {
- _entityManager = new WeakReference(dependencies.Resolve());
- }
-
public override void Write(Utf8JsonWriter writer, SerializablePlayer value, JsonSerializerOptions options)
{
writer.WriteStartObject();
- if (value.Player.AttachedEntity is {Valid: true} playerEntity)
+ if (value.Uid is {Valid: true} playerEntity)
{
- if (!_entityManager.TryGetTarget(out var entityManager))
- throw new InvalidOperationException("EntityManager got garbage collected!");
-
- writer.WriteNumber("id", (int) value.Player.AttachedEntity);
- writer.WriteString("name", entityManager.GetComponent(playerEntity).EntityName);
+ writer.WriteNumber("id", playerEntity.Id);
+ writer.WriteString("name", value.Name);
}
- writer.WriteString("player", value.Player.UserId.UserId);
+ writer.WriteString("player", value.UserId);
writer.WriteEndObject();
}
}
-
-public readonly struct SerializablePlayer
-{
- public readonly ICommonSession Player;
-
- public SerializablePlayer(ICommonSession player)
- {
- Player = player;
- }
-}
diff --git a/Content.Server/Administration/Systems/AdminFrozenSystem.cs b/Content.Server/Administration/Systems/AdminFrozenSystem.cs
deleted file mode 100644
index baf7b682b85f..000000000000
--- a/Content.Server/Administration/Systems/AdminFrozenSystem.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Shared.Administration;
-
-namespace Content.Server.Administration.Systems;
-
-public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
-{
- ///
- /// Freezes and mutes the given entity.
- ///
- public void FreezeAndMute(EntityUid uid)
- {
- var comp = EnsureComp(uid);
- comp.Muted = true;
- Dirty(uid, comp);
- }
-}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
index b1721c9d807f..bffea7d68cc5 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
@@ -1,10 +1,8 @@
using System.Threading;
using Content.Server.Administration.Components;
-using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
-using Content.Server.Clothing.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
using Content.Server.GhostKick;
@@ -14,13 +12,13 @@
using Content.Server.Polymorph.Systems;
using Content.Server.Popups;
using Content.Server.Speech.Components;
-using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Server.Tabletop;
using Content.Server.Tabletop.Components;
using Content.Shared.Administration;
using Content.Shared.Administration.Components;
-using Content.Shared.Alert;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Alert; //Starlight
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Clumsy;
@@ -30,6 +28,7 @@
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Electrocution;
+using Content.Shared.Gravity;
using Content.Shared.Interaction.Components;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
@@ -40,7 +39,7 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Slippery;
-using Content.Shared.Stunnable;
+using Content.Shared.Storage.Components;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
@@ -52,7 +51,6 @@
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
-using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -539,7 +537,7 @@ private void AddSmiteVerbs(GetVerbsEvent args)
{
Text = nyanifyName,
Category = VerbCategory.Smite,
- Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
+ Icon = new SpriteSpecifier.Rsi(new ("/Textures/_Starlight/Clothing/Head/Hats/blackcatears.rsi"), "icon"),
Act = () =>
{
var ears = Spawn("ClothingHeadHatCatEars", Transform(args.Target).Coordinates);
@@ -552,6 +550,25 @@ private void AddSmiteVerbs(GetVerbsEvent args)
};
args.Verbs.Add(nyanify);
+ // Starlight - Start
+ var nyanify2Name = Loc.GetString("admin-smite-nyanify-name2").ToLowerInvariant();
+ Verb nyanify2 = new()
+ {
+ Text = nyanify2Name,
+ Category = VerbCategory.Smite,
+ Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
+ Act = () =>
+ {
+ var ears = Spawn("ClothingHeadHatWhiteCatEars", Transform(args.Target).Coordinates);
+ EnsureComp(ears);
+ _inventorySystem.TryUnequip(args.Target, "head", true, true, false, inventory);
+ _inventorySystem.TryEquip(args.Target, ears, "head", true, true, false, inventory);
+ },
+ Impact = LogImpact.Extreme,
+ Message = string.Join(": ", nyanify2Name, Loc.GetString("admin-smite-nyanify2-description"))
+ };
+ args.Verbs.Add(nyanify2);
+ // Starlight - End
var killSignName = Loc.GetString("admin-smite-kill-sign-name").ToLowerInvariant();
Verb killSign = new()
{
@@ -680,6 +697,11 @@ private void AddSmiteVerbs(GetVerbsEvent args)
grav.Weightless = true;
Dirty(args.Target, grav);
+
+ EnsureComp(args.Target, out var weightless);
+ weightless.Weightless = true;
+
+ Dirty(args.Target, weightless);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", noGravityName, Loc.GetString("admin-smite-remove-gravity-description"))
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
index f38af4f17e60..03704126a870 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
@@ -6,6 +6,7 @@
using Content.Server.Cargo.Components;
using Content.Server.Doors.Systems;
using Content.Server.Hands.Systems;
+using Content.Server._Starlight.Thaven; // 🌟Starlight🌟
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
@@ -26,7 +27,7 @@
using Content.Shared.Doors.Components;
using Content.Shared.Electrocution;
using Content.Shared.Hands.Components;
-using Content.Shared.Humanoid;
+using Content.Shared._Starlight.Thaven.Components; // 🌟Starlight🌟
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Stacks;
@@ -41,8 +42,9 @@
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
-using Content.Shared.Overlays; // 🌟Starlight🌟
+using Content.Shared.Overlays;
using Content.Shared.Contraband; // 🌟Starlight🌟
+using Content.Shared.Humanoid; // 🌟Starlight🌟
namespace Content.Server.Administration.Systems;
@@ -62,10 +64,10 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
- //🌟Starlight🌟 start
+ #region Starlight
[Dependency] private readonly LimbSystem _limbSystem = default!;
[Dependency] private readonly StarlightEntitySystem _entitySystem = default!;
- //🌟Starlight🌟 end
+ #endregion
private void AddTricksVerbs(GetVerbsEvent args)
{
@@ -94,31 +96,30 @@ private void AddTricksVerbs(GetVerbsEvent args)
Message = Loc.GetString(bolts.BoltsDown
? "admin-trick-unbolt-description"
: "admin-trick-bolt-description"),
- Priority = (int) (bolts.BoltsDown ? TricksVerbPriorities.Unbolt : TricksVerbPriorities.Bolt),
+ Priority = (int)(bolts.BoltsDown ? TricksVerbPriorities.Unbolt : TricksVerbPriorities.Bolt),
};
args.Verbs.Add(bolt);
}
-
+
if (TryComp(args.Target, out var electrified) && HasComp(args.Target))
+ {
+ Verb electrify = new()
{
- Verb electrify = new()
+ Text = electrified.Enabled ? "Unelectrify" : "Electrify",
+ Category = VerbCategory.Tricks,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
+ Act = () =>
{
- Text = electrified.Enabled ? "Unelectrify" : "Electrify",
- Category = VerbCategory.Tricks,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
- Act = () =>
- {
- _electrocution.SetElectrified((args.Target, electrified), !electrified.Enabled);
- },
- Impact = LogImpact.Medium,
- Message = Loc.GetString(electrified.Enabled
- ? "admin-trick-unelectrify-description"
- : "admin-trick-electrify-description"),
- Priority = (int) (electrified.Enabled ? TricksVerbPriorities.Unelectrify : TricksVerbPriorities.Electrify),
- };
- args.Verbs.Add(electrify);
- }
-
+ _electrocution.SetElectrified((args.Target, electrified), !electrified.Enabled);
+ },
+ Impact = LogImpact.Medium,
+ Message = Loc.GetString(electrified.Enabled
+ ? "admin-trick-unelectrify-description"
+ : "admin-trick-electrify-description"),
+ Priority = (int) (electrified.Enabled ? TricksVerbPriorities.Unelectrify : TricksVerbPriorities.Electrify),
+ };
+ args.Verbs.Add(electrify);
+ }
if (TryComp(args.Target, out var airlockComp))
{
@@ -451,7 +452,7 @@ private void AddTricksVerbs(GetVerbsEvent