From a05e8768799797b417757ab375b337f52a63fa6d Mon Sep 17 00:00:00 2001 From: MureixloI <132683811+MureixloI@users.noreply.github.com> Date: Thu, 8 May 2025 20:38:31 +0300 Subject: [PATCH 001/407] changing sprite of mime satchel (#37280) --- .../Satchels/mime.rsi/equipped-BACKPACK.png | Bin 674 -> 476 bytes .../Clothing/Back/Satchels/mime.rsi/icon.png | Bin 791 -> 723 bytes .../Back/Satchels/mime.rsi/inhand-left.png | Bin 644 -> 629 bytes .../Back/Satchels/mime.rsi/inhand-right.png | Bin 613 -> 618 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Resources/Textures/Clothing/Back/Satchels/mime.rsi/equipped-BACKPACK.png b/Resources/Textures/Clothing/Back/Satchels/mime.rsi/equipped-BACKPACK.png index ea4861dfc212bb1196295f9a2c5f1369c10ffd0c..d40037aad7c779db29759a4a122ce20cdd0f3560 100644 GIT binary patch delta 438 zcmV;n0ZIO%1>6IWFnX_0|N(&RK>zITri0naA{QH z)D1XS7lz^71^i1^i^akQB%9<=i~kBB|4<`Sk|{z&L_|dNGF8Re#(&E>n5q_A1XYEq zmKZt@Gb?)aYtOSt?NCiELR6S079l!B=={;ePeO9EQv9S!9)F0uxjRbl_0LjOxH};7 zUStAy2Y@Dvp8$Zn=g&A5pHgai0a8jRvIyfimRZ>X+#TaM&dxgPQTTejKA$K+_Pl1+ z?2q41s){AM!mJMH_xr!kN2>4R-@f6uCL$stA|fIpBKk!lz8ij?8;JjA8C86AhOo&W z&9^j1)*~{tsDCPMx7(lS9A9c){qTGJo+d-hw8~o`r$q~}<}3Zk%N?+me~q)aS8TCE z36;$50Cz{%b@Kg?1ON=fAYIq3wi9OWrDlfBW+U6}RzyU0yPXWfAR;2C(@DKDyDNdlax~Yi^vNWx5~X@ gn|drIA|jgYKfI8=Iso(o!2kdN07*qoM6N<$f~jB7Gynhq delta 637 zcmV-@0)qYA1EK|xFnrw!>lAWPjHS%qxQH1ssM=9*w7^ z1}Ed_EW*2@9k^b=b>F9@Qq*dG+|d!#OYUyxJ4>ag?)#*x2)jiZOY+w@H*|M9FEx4b zG!EB&pNNQyj=OPV%Xawi;EjnH_f1mDu-_zeezcwC!qlT94@ZR2gisOeu zgBPVEj@*q*Nq=5fJeeBw`U55+I{o&QOiErPi}3x&Pa-1v^0}iYzFvR8mhJFxGGo^Z z*wmL1%XZlH0v=9gY}pRWMN59=;CcbK+n?5smp2A2 z-_2$3KKSpb3z_y+24qa~Lfa}- z@YUt~*>>->ZiB8a=VbcUd0}$Dw~pHX5?!d6TSx6m*MYQg^2U+tF_PSV7FBPXyI%0> z*}$~_SLF_R-HS%m&*PVp%$qy_008{o9swfqRJ`ecXhHx001jnXNoGw=04e|g00;m8 X000000Mb*F00000NkvXXu0mjfAY(Y! diff --git a/Resources/Textures/Clothing/Back/Satchels/mime.rsi/icon.png b/Resources/Textures/Clothing/Back/Satchels/mime.rsi/icon.png index 52f7806cbb1ca0272b540429d6ba69fc0524b00a..04ccb19198518918b57d9275b136966e546167e1 100644 GIT binary patch delta 686 zcmV;f0#W^!2Ga$QF@KFoL_t(oh3%H1ZtF@AhQC}L=#>hc)TkgTieyyD)ba~hCV_o} zfbtVoQmK84m{ev(RcNK6N+oFnijtUGviTfvv#WJtI|)2G`K2g3v$H$@&d!V%&N$;= zNAHlqU@(xfECDFX@}NC{QcAY}Y&M(DHNOry_dJiHD6~ork$+H1Nx$C*;Ns!}K)OhD z60k8EjTnta0CJ4|=jW#x4~IhmST2`)L^=s+xKBuNAyj$@;{ySoEmyX}ae)%d#5rHh(l;fnCeK@7FdCg22d+kB_*n z%lG#;06`Gc9Q(dcmSyHJz+Pk8y8`XJs;WI=^ZA^Qj}H@#+uK`Q*X92H9{4|6zV8!; z;ojP|Zdwy;=Z@n5rkeqdNTCFvRmbBkL|&RaIkK?yQ2Usv0Aq zOK`Uob$?~7R;!v`AEpT=3GI7#9EUv5aU2KF^UR6tL~Db#(d+bHZQp~&2rQS&o}D;N z*zE&Njp-P)mfR8EapKQmQkd z^jckV)?;^gB*o~+ z2mnUau;bB46g8{~r!&34s z((;O9{?uf)D-S?V>zJmAVXU8Q0pp3J(DiX47f=mrLJeyoo=Dn1xm=wwnbi^p*U z(paRa%;jf!wy?=&^xOe{{u~>|`{H(7Rb#*nPYLJ=8&}y}Sw!u6BhKg^;2EuxZqeC6+mz=e*F>fyQ2gXe0{Q zPA25F>LoZ1(s(TE&~^UMvxM@?*LiQ9yg+`GdTUXb&)cLDUgUiO+(cxvSA}Vs_AGn5 zA|Z*7BlQ%}DFTjeL}RaE7j`q3lMs6f0MPYuA&F00e1HK4@Pj{&scB1VWt@-z000hU kSV?A0O#mtY000O800000007cclK=n!07*qoM6N<$f=(E9+yDRo diff --git a/Resources/Textures/Clothing/Back/Satchels/mime.rsi/inhand-left.png b/Resources/Textures/Clothing/Back/Satchels/mime.rsi/inhand-left.png index f471b672b2adbb785aa4c931bc20a1400401c25d..7c034cef7ed2b380b92bdb2e4d217fbfec5cb9f8 100644 GIT binary patch delta 591 zcmV-V0ND9Hde&8uepm=cz#PDfhU*aNeDcDfHB4xV~jDz z7-NjFn@K52N=g5F9bLTjEg*!T{eF+xYzD3MQ*MGctu?JyD}Qp%1>b-)P3im?ns*1Z z%CdyD7U!Gb+Ywr8O4Ah1Im+|gf00+b^DUsYrX)#Vj6qdZm`o<82Tzg&04UG%H(l%Y z?dFADilT_3D8hQZhP4)h!2pND0dX8-KA)p$8Z=FF^L;Ms#&L}4bn3m{;EJLkr4$Jv z$T>$uM9w)9LVu7_ii)D>^cq~&U9DEVd;`2+fh@}~8jSz|)>>F=PuJ?YMwVqk>h>i_ zOGQyczmBJKP1Cg3u-olA-KVWP9*+S4o6Y9&UV)Yn;%bZj>HKSbh~GcYzs0qO_1ysA z$u>bsNlGbFN(KEr{+xY$o=sqkF~%5Uj4{R-V~nwoLVxBFf=!3d^_dfR_&h>8$Ln)( zA@c}9JHR@Qh8M5qEq4EepH({J1S4fCFLNNCjXZMvy@OIBxeuz9muvYo$f~7^OgZmc_Qlv?LL5{FmoJo;fVdE8oSiclum*w0Xap%p&knacEZ1K(8 z*_mnP0YpSZL_|bHL`3&ytP^9M{OvlbcoZ67tP@}NSZJ&P0Dty&H-a<&DaJZEI6RSF ze-LFA^!ft_T#8@ZKp3mk_c%^qe2@NpT$-CqC*niUk z0DNtJLssnm+hc6E)@5{cdGBqJO8{9RN^u0m*5@L_o2D%PFJ$CKwIz@0D#eG1OPZsFG}Bs=9Xn^ zQCU)x{_@PsQ~daTQE8m<+$vjXYJPW0ho0WCPX8#%iq-gln3JHsq`aROV+?>_dpJ!` zRmSjsUbM1^h=_=Yh=_=Yh*ZVy7tqXqtPwJ9b882_?tg=Chs$LB2%B3wI5<2B&R4k_ zD>Bx}^6DZ0AU#X9;{gB*jWsN)ns@O6*$ER8Y&S@wsIpYr!r$5cB6wR;05 z(d(^b diff --git a/Resources/Textures/Clothing/Back/Satchels/mime.rsi/inhand-right.png b/Resources/Textures/Clothing/Back/Satchels/mime.rsi/inhand-right.png index 4d23a7000aa00bb2ad491e29031f198bd02e8df5..fd54558b1e5ec4cf44d094d6f34a5fd01545c987 100644 GIT binary patch delta 580 zcmV-K0=xa?1nLBkF@GdUL_t(|obB2nkK#ZS2k`%1&g>-(SVkhM2uV#94uz#7ioBrj zbMOJ~1Dq?S!Byf;cjP39O)w?rN}90e-ib>IccHsX7?`qoKSO9q=gsiiiunVKF~%5U zj4{R-V~oAY_kHqxpIp~r%O{f`ODSnM9AdZIK?w0-7U5!AYkx|TgrtngmlG)4cP$rqd|^5C9ww2dq{r06={_P17eU zRo}=ZbvB;xJdZrj!+O1jQVN|;2mAdVK@ea(9;2!%R8`fw1*kjq>N>M5qc99f2titF zA|ldSlMsT!Fn^>h%UbyyNRq^;&c<6Hj$`zCJph1G3QDQtxw0$~$FZ%Nt*dLjq|V0H z5JFH@RR93b^Nz>sV{L4I02mAg1OVIZ7QhvCHofN`<^4Yj`q$NYZlaGc`EOsRWHR~2 zXFyRDPrmO{x7!8q_kP-JHmz*?Q-{}A{8TTkuahyx7=L4oF~%5Uj4}4W9yv2$ln*%P zUYUW`Zpq&xGa#j;de6`HwT(N#Rc0X1b10=?dl4EpM$U1W8JNvxSS%LD@oX_F)y>E;9r3ctWF~PxE+Ubv)xXa-A8N#}oSU{EY8ER-6G~7W@I=I&vor S2$U570000J(11my&fF@FU~L_t(|obB2%OCv!T2H^L^pD^2Ok)(=ErqF=3OAdo~;NP$k z3$d~a1RJqTVHLcrhw*x$P{7{q768D-cYrDVtMEEoPUZO$1a&c=dvlGh+l_8V zuRn zyFL6=<)L*|lv0SI2w`~PWa%40P?xWelZc3jh=_=Yh=|6^yB)t^W0Sl>D$f^P(b(SE zx4m^N^8qxPE!p1Lhpy6eIDlEs42)~h+rCHU zKzYs#n0HIN)0NS_zs-B3>YmJcX284z!^;pSr)Tl{>hhAcd!!nY0svjn*xTKD6X@4^ z029=;u~e4Zn`?I(o%5I(aIPgkkC_3js}clt z(YiXnZ#aSXfxgPj0MTU6+c`n#xVag{0000EWmrjOO-%qQ00008000000002eQ Date: Thu, 8 May 2025 17:39:38 +0000 Subject: [PATCH 002/407] Automatic changelog update --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 41442ab1be..b5b3323efa 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -3921,3 +3921,10 @@ id: 8441 time: '2025-05-08T16:49:59.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/37281 +- author: MureixloL + changes: + - message: Changed sprite of mime satchel! + type: Tweak + id: 8442 + time: '2025-05-08T17:38:31.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/37280 From 7bec14863440fbf51fd286fbb899ad33c0119057 Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Thu, 8 May 2025 15:53:19 -0400 Subject: [PATCH 003/407] Validate Cargo Markets (#37271) * Validate cargo markets * readonly market ID --- .../Cargo/Components/CargoOrderConsoleComponent.cs | 2 +- .../Cargo/Prototypes/CargoMarketPrototype.cs | 14 ++++++++++++++ .../Cargo/Prototypes/CargoProductPrototype.cs | 2 +- Resources/Prototypes/Catalog/Cargo/markets.yml | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 Content.Shared/Cargo/Prototypes/CargoMarketPrototype.cs create mode 100644 Resources/Prototypes/Catalog/Cargo/markets.yml diff --git a/Content.Shared/Cargo/Components/CargoOrderConsoleComponent.cs b/Content.Shared/Cargo/Components/CargoOrderConsoleComponent.cs index 8b189313ae..44790d8881 100644 --- a/Content.Shared/Cargo/Components/CargoOrderConsoleComponent.cs +++ b/Content.Shared/Cargo/Components/CargoOrderConsoleComponent.cs @@ -78,7 +78,7 @@ public sealed partial class CargoOrderConsoleComponent : Component /// All of the s that are supported. /// [DataField, AutoNetworkedField] - public List AllowedGroups = new() { "market" }; + public List> AllowedGroups = new() { "market" }; /// /// Access needed to toggle the limit on this console. diff --git a/Content.Shared/Cargo/Prototypes/CargoMarketPrototype.cs b/Content.Shared/Cargo/Prototypes/CargoMarketPrototype.cs new file mode 100644 index 0000000000..32fe3a4dac --- /dev/null +++ b/Content.Shared/Cargo/Prototypes/CargoMarketPrototype.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Cargo.Prototypes; + +/// +/// Defines a "market" that a cargo computer can access and make orders from. +/// +[Prototype] +public sealed partial class CargoMarketPrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; private set; } = default!; +} diff --git a/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs b/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs index 5a18b6becc..d98c9bd8f7 100644 --- a/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs +++ b/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs @@ -93,6 +93,6 @@ namespace Content.Shared.Cargo.Prototypes /// The prototype group of the product. (e.g. Contraband) /// [DataField] - public string Group { get; private set; } = "market"; + public ProtoId Group { get; private set; } = "market"; } } diff --git a/Resources/Prototypes/Catalog/Cargo/markets.yml b/Resources/Prototypes/Catalog/Cargo/markets.yml new file mode 100644 index 0000000000..b6d8790a8f --- /dev/null +++ b/Resources/Prototypes/Catalog/Cargo/markets.yml @@ -0,0 +1,2 @@ +- type: cargoMarket + id: market From 2a201837c7bfab2ef3bffa40bb45859579328dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Walsh?= Date: Fri, 9 May 2025 01:06:26 +0100 Subject: [PATCH 004/407] Link to reagent ingredients on the same Guidebook page (#36700) * Add in-page links for guidebook reagent recipes * Add links to microwave recipes * This function is too specific to be in Control extensions * Better naming * Wrap RichTextLabel instead of subclassing * "Activate" is ambiguous --- .../Controls/GuideMicrowaveEmbed.xaml.cs | 12 ++- .../Controls/GuideReagentEmbed.xaml.cs | 6 +- .../Controls/GuideReagentReaction.xaml | 18 ++--- .../Controls/GuideReagentReaction.xaml.cs | 63 ++++++++------- .../Controls/GuidebookRichPrototypeLink.cs | 71 +++++++++++++++++ .../Controls/GuidebookWindow.xaml.cs | 79 ++++++++++++++++++- .../Controls/IPrototypeLinkControl.cs | 28 +++++++ .../Guidebook/Richtext/TextLinkTag.cs | 18 ++--- .../ControlExtensions/ControlExtension.cs | 48 +++++++++++ 9 files changed, 287 insertions(+), 56 deletions(-) create mode 100644 Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs create mode 100644 Content.Client/Guidebook/Controls/IPrototypeLinkControl.cs diff --git a/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs index 1ae09fc8fe..da93fb46fd 100644 --- a/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs @@ -19,13 +19,15 @@ namespace Content.Client.Guidebook.Controls; /// Control for embedding a microwave recipe into a guidebook. /// [UsedImplicitly, GenerateTypedNameReferences] -public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, ISearchableControl +public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl { [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly ILogManager _logManager = default!; private ISawmill _sawmill = default!; + public IPrototype? RepresentedPrototype { get; private set; } + public GuideMicrowaveEmbed() { RobustXamlLoader.Load(this); @@ -80,6 +82,8 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, { var entity = _prototype.Index(recipe.Result); + RepresentedPrototype = entity; + IconContainer.AddChild(new GuideEntityEmbed(recipe.Result, false, false)); ResultName.SetMarkup(entity.Name); ResultDescription.SetMarkup(entity.Description); @@ -99,8 +103,9 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, solidNameMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-solid-name-display", ("ingredient", ingredient.Name))); solidNameMsg.Pop(); - var solidNameLabel = new RichTextLabel(); + var solidNameLabel = new GuidebookRichPrototypeLink(); solidNameLabel.SetMessage(solidNameMsg); + solidNameLabel.LinkedPrototype = ingredient; IngredientsGrid.AddChild(solidNameLabel); @@ -129,9 +134,10 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, liquidColorMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-reagent-color-display", ("color", reagent.SubstanceColor))); liquidColorMsg.Pop(); - var liquidColorLabel = new RichTextLabel(); + var liquidColorLabel = new GuidebookRichPrototypeLink(); liquidColorLabel.SetMessage(liquidColorMsg); liquidColorLabel.HorizontalAlignment = Control.HAlignment.Center; + liquidColorLabel.LinkedPrototype = reagent; IngredientsGrid.AddChild(liquidColorLabel); diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs index f8d1c7e972..78cd765bdb 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs @@ -22,13 +22,15 @@ namespace Content.Client.Guidebook.Controls; /// Control for embedding a reagent into a guidebook. /// [UsedImplicitly, GenerateTypedNameReferences] -public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl +public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl { [Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; private readonly ChemistryGuideDataSystem _chemistryGuideData; + public IPrototype? RepresentedPrototype { get; private set; } + public GuideReagentEmbed() { RobustXamlLoader.Load(this); @@ -80,6 +82,8 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea private void GenerateControl(ReagentPrototype reagent) { + RepresentedPrototype = reagent; + NameBackground.PanelOverride = new StyleBoxFlat { BackgroundColor = reagent.SubstanceColor diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml index 5b871644ea..b84d833628 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml +++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml @@ -4,13 +4,11 @@ HorizontalExpand="True" Margin="0 0 0 5"> - - + + + - + + + diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs index 135dc5522a..29ed124422 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml.cs @@ -34,16 +34,16 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont public GuideReagentReaction(ReactionPrototype prototype, IPrototypeManager protoMan, IEntitySystemManager sysMan) : this(protoMan) { - var reactantsLabel = ReactantsLabel; - SetReagents(prototype.Reactants, ref reactantsLabel, protoMan); - var productLabel = ProductsLabel; + Container container = ReactantsContainer; + SetReagents(prototype.Reactants, ref container, protoMan); + Container productContainer = ProductsContainer; var products = new Dictionary(prototype.Products); foreach (var (reagent, reactantProto) in prototype.Reactants) { if (reactantProto.Catalyst) products.Add(reagent, reactantProto.Amount); } - SetReagents(products, ref productLabel, protoMan); + SetReagents(products, ref productContainer, protoMan, false); var mixingCategories = new List(); if (prototype.MixingCategories != null) @@ -85,8 +85,8 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont entContainer.AddChild(nameLabel); ReactantsContainer.AddChild(entContainer); - var productLabel = ProductsLabel; - SetReagents(solution.Contents, ref productLabel, protoMan); + Container productContainer = ProductsContainer; + SetReagents(solution.Contents, ref productContainer, protoMan, false); SetMixingCategory(categories, null, sysMan); } @@ -95,75 +95,80 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont IPrototypeManager protoMan, IEntitySystemManager sysMan) : this(protoMan) { - ReactantsLabel.Visible = true; - ReactantsLabel.SetMarkup(Loc.GetString("guidebook-reagent-sources-gas-wrapper", + var label = new RichTextLabel(); + label.SetMarkup(Loc.GetString("guidebook-reagent-sources-gas-wrapper", ("name", Loc.GetString(prototype.Name).ToLower()))); + ReactantsContainer.Visible = true; + ReactantsContainer.AddChild(label); + if (prototype.Reagent != null) { var quantity = new Dictionary { { prototype.Reagent, FixedPoint2.New(0.21f) } }; - var productLabel = ProductsLabel; - SetReagents(quantity, ref productLabel, protoMan); + Container productContainer = ProductsContainer; + SetReagents(quantity, ref productContainer, protoMan, false); } SetMixingCategory(categories, null, sysMan); } - private void SetReagents(List reagents, ref RichTextLabel label, IPrototypeManager protoMan) + private void SetReagents(List reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true) { var amounts = new Dictionary(); foreach (var (reagent, quantity) in reagents) { amounts.Add(reagent.Prototype, quantity); } - SetReagents(amounts, ref label, protoMan); + SetReagents(amounts, ref container, protoMan, addLinks); } private void SetReagents( Dictionary reactants, - ref RichTextLabel label, - IPrototypeManager protoMan) + ref Container container, + IPrototypeManager protoMan, + bool addLinks = true) { var amounts = new Dictionary(); foreach (var (reagent, reactantPrototype) in reactants) { amounts.Add(reagent, reactantPrototype.Amount); } - SetReagents(amounts, ref label, protoMan); + SetReagents(amounts, ref container, protoMan, addLinks); } [PublicAPI] private void SetReagents( Dictionary, ReactantPrototype> reactants, - ref RichTextLabel label, - IPrototypeManager protoMan) + ref Container container, + IPrototypeManager protoMan, + bool addLinks = true) { var amounts = new Dictionary(); foreach (var (reagent, reactantPrototype) in reactants) { amounts.Add(reagent, reactantPrototype.Amount); } - SetReagents(amounts, ref label, protoMan); + SetReagents(amounts, ref container, protoMan, addLinks); } - private void SetReagents(Dictionary reagents, ref RichTextLabel label, IPrototypeManager protoMan) + private void SetReagents(Dictionary reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true) { - var msg = new FormattedMessage(); - var reagentCount = reagents.Count; - var i = 0; foreach (var (product, amount) in reagents.OrderByDescending(p => p.Value)) { + var productProto = protoMan.Index(product); + var msg = new FormattedMessage(); msg.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-recipes-reagent-display", - ("reagent", protoMan.Index(product).LocalizedName), ("ratio", amount))); - i++; - if (i < reagentCount) - msg.PushNewline(); + ("reagent", productProto.LocalizedName), ("ratio", amount))); + + var label = new GuidebookRichPrototypeLink(); + if (addLinks) + label.LinkedPrototype = productProto; + label.SetMessage(msg); + container.AddChild(label); } - msg.Pop(); - label.SetMessage(msg); - label.Visible = true; + container.Visible = true; } private void SetMixingCategory(IReadOnlyList> mixingCategories, ReactionPrototype? prototype, IEntitySystemManager sysMan) diff --git a/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs b/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs new file mode 100644 index 0000000000..b54dd8b701 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuidebookRichPrototypeLink.cs @@ -0,0 +1,71 @@ +using Content.Client.Guidebook.RichText; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Input; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using Content.Client.UserInterface.ControlExtensions; + +namespace Content.Client.Guidebook.Controls; + +/// +/// A RichTextLabel which is a link to a specified IPrototype. +/// The link is activated by the owner if the prototype is represented +/// somewhere in the same document. +/// +public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl +{ + private bool _linkActive = false; + private FormattedMessage? _message; + private readonly RichTextLabel _richTextLabel; + + public void EnablePrototypeLink() + { + if (_message == null) + return; + + _linkActive = true; + + DefaultCursorShape = CursorShape.Hand; + + _richTextLabel.SetMessage(_message, null, TextLinkTag.LinkColor); + } + + public GuidebookRichPrototypeLink() : base() + { + MouseFilter = MouseFilterMode.Pass; + OnKeyBindDown += HandleClick; + _richTextLabel = new RichTextLabel(); + AddChild(_richTextLabel); + } + + public void SetMessage(FormattedMessage message) + { + _message = message; + _richTextLabel.SetMessage(_message); + } + + public IPrototype? LinkedPrototype { get; set; } + + private void HandleClick(GUIBoundKeyEventArgs args) + { + if (!_linkActive) + return; + + if (args.Function != EngineKeyFunctions.UIClick) + return; + + if (this.TryGetParentHandler(out var handler)) + { + handler.HandleAnchor(this); + args.Handle(); + } + else + Logger.Warning("Warning! No valid IAnchorClickHandler found."); + } +} + +public interface IAnchorClickHandler +{ + public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl); +} diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs index 5d2d227b3d..13ee0c87e7 100644 --- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Linq; using Content.Client.Guidebook.RichText; using Content.Client.UserInterface.ControlExtensions; @@ -6,6 +7,7 @@ using Content.Client.UserInterface.Controls.FancyTree; using Content.Client.UserInterface.Systems.Info; using Content.Shared.Guidebook; using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.ContentPack; @@ -14,7 +16,7 @@ using Robust.Shared.Prototypes; namespace Content.Client.Guidebook.Controls; [GenerateTypedNameReferences] -public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler +public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IAnchorClickHandler { [Dependency] private readonly DocumentParsingManager _parsingMan = default!; [Dependency] private readonly IResourceManager _resourceManager = default!; @@ -53,6 +55,38 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler ShowGuide(entry); } + public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl) + { + var prototype = prototypeLinkControl.LinkedPrototype; + if (prototype == null) + return; + + var (linkableControls, _) = GetLinkableControlsAndLinks(EntryContainer); + foreach (var linkableControl in linkableControls) + { + if (linkableControl.RepresentedPrototype != prototype) + continue; + + if (linkableControl is not Control control) + return; + + // Check if the target item is currently filtered out + if (!control.Visible) + control.Visible = true; + + UserInterfaceManager.DeferAction(() => + { + if (control.GetControlScrollPosition() is not {} position) + return; + + Scroll.HScrollTarget = position.X; + Scroll.VScrollTarget = position.Y; + }); + + break; + } + } + private void OnSelectionChanged(TreeItem? item) { if (item != null && item.Metadata is GuideEntry entry) @@ -94,6 +128,23 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler } LastEntry = entry.Id; + + var (linkableControls, linkControls) = GetLinkableControlsAndLinks(EntryContainer); + + HashSet availablePrototypeLinks = new(); + foreach (var linkableControl in linkableControls) + { + var prototype = linkableControl.RepresentedPrototype; + if (prototype != null) + availablePrototypeLinks.Add(prototype); + } + + foreach (var linkControl in linkControls) + { + var prototype = linkControl.LinkedPrototype; + if (prototype != null && availablePrototypeLinks.Contains(prototype)) + linkControl.EnablePrototypeLink(); + } } public void UpdateGuides( @@ -209,4 +260,30 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler } } } + + private static (List, List) GetLinkableControlsAndLinks(Control parent) + { + List linkableList = new(); + List linkList = new(); + + foreach (var child in parent.Children) + { + var hasChildren = child.ChildCount > 0; + + if (child is IPrototypeLinkControl linkChild) + linkList.Add(linkChild); + else if (child is IPrototypeRepresentationControl linkableChild) + linkableList.Add(linkableChild); + + if (!hasChildren) + continue; + + var (childLinkableList, childLinkList) = GetLinkableControlsAndLinks(child); + + linkableList.AddRange(childLinkableList); + linkList.AddRange(childLinkList); + } + + return (linkableList, linkList); + } } diff --git a/Content.Client/Guidebook/Controls/IPrototypeLinkControl.cs b/Content.Client/Guidebook/Controls/IPrototypeLinkControl.cs new file mode 100644 index 0000000000..51406e3cc2 --- /dev/null +++ b/Content.Client/Guidebook/Controls/IPrototypeLinkControl.cs @@ -0,0 +1,28 @@ +using Robust.Shared.Prototypes; + +namespace Content.Client.Guidebook.Controls; + +/// +/// Interface for controls which represent a Prototype +/// These can be linked to from a IPrototypeLinkControl +/// +public interface IPrototypeRepresentationControl +{ + // The prototype that this control represents + public IPrototype? RepresentedPrototype { get; } +} + +/// +/// Interface for controls which can be clicked to navigate +/// to a specified prototype representation on the same page. +/// +public interface IPrototypeLinkControl +{ + // This control is a link to the specified prototype + public IPrototype? LinkedPrototype { get; } + + // Initially the link will not be enabled, + // the owner can enable the link once there is a valid target + // for the Prototype link. + public void EnablePrototypeLink(); +} diff --git a/Content.Client/Guidebook/Richtext/TextLinkTag.cs b/Content.Client/Guidebook/Richtext/TextLinkTag.cs index b1e8460bb8..27aaa71939 100644 --- a/Content.Client/Guidebook/Richtext/TextLinkTag.cs +++ b/Content.Client/Guidebook/Richtext/TextLinkTag.cs @@ -5,12 +5,15 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.RichText; using Robust.Shared.Input; using Robust.Shared.Utility; +using Content.Client.UserInterface.ControlExtensions; namespace Content.Client.Guidebook.RichText; [UsedImplicitly] public sealed class TextLinkTag : IMarkupTag { + public static Color LinkColor => Color.CornflowerBlue; + public string Name => "textlink"; public Control? Control; @@ -30,7 +33,7 @@ public sealed class TextLinkTag : IMarkupTag label.Text = text; label.MouseFilter = Control.MouseFilterMode.Stop; - label.FontColorOverride = Color.CornflowerBlue; + label.FontColorOverride = LinkColor; label.DefaultCursorShape = Control.CursorShape.Hand; label.OnMouseEntered += _ => label.FontColorOverride = Color.LightSkyBlue; @@ -50,17 +53,10 @@ public sealed class TextLinkTag : IMarkupTag if (Control == null) return; - var current = Control; - while (current != null) - { - current = current.Parent; - - if (current is not ILinkClickHandler handler) - continue; + if (Control.TryGetParentHandler(out var handler)) handler.HandleClick(link); - return; - } - Logger.Warning($"Warning! No valid ILinkClickHandler found."); + else + Logger.Warning("Warning! No valid ILinkClickHandler found."); } } diff --git a/Content.Client/UserInterface/ControlExtensions/ControlExtension.cs b/Content.Client/UserInterface/ControlExtensions/ControlExtension.cs index c0e4a038a1..a0e5a1063c 100644 --- a/Content.Client/UserInterface/ControlExtensions/ControlExtension.cs +++ b/Content.Client/UserInterface/ControlExtensions/ControlExtension.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; using Content.Client.Guidebook.Controls; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -68,6 +70,52 @@ public static class ControlExtension return controlList; } + /// + /// Search the control’s tree for a parent node of type T + /// E.g. to find the control implementing some event handling interface. + /// + public static bool TryGetParentHandler(this Control child, [NotNullWhen(true)] out T? result) + { + for (var control = child; control is not null; control = control.Parent) + { + if (control is not T handler) + continue; + + result = handler; + return true; + } + + result = default; + return false; + } + + /// + /// Find the control’s offset relative to its closest ScrollContainer + /// Returns null if the control is not in the tree or not visible. + /// + public static Vector2? GetControlScrollPosition(this Control child) + { + if (!child.VisibleInTree) + return null; + + var position = new Vector2(); + var control = child; + + while (control is not null) + { + // The scroll container's direct child is re-positioned while scrolling, + // so we need to ignore its position. + if (control.Parent is ScrollContainer) + break; + + position += control.Position; + + control = control.Parent; + } + + return position; + } + public static bool ChildrenContainText(this Control parent, string search) { var labels = parent.GetControlOfType