Skip to content

Commit c07f4c9

Browse files
authored
Merge branch 'starlight-dev' into starboardSM
2 parents 7b82b71 + 06303f3 commit c07f4c9

353 files changed

Lines changed: 6379 additions & 283 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Content.Client/_Moffstation/Antags/UI/AntagEntry.xaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
HorizontalAlignment="Center"
2727
SetSize="20 20"
2828
Stretch="KeepAspectCentered"
29+
MouseFilter="Stop"
2930
TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png"
30-
Visible="False"/>
31+
Visible="False"/> <!-- Starlight, enable unlock requirements again -->
3132
</BoxContainer>
3233

3334
<!-- Trait Info -->

Content.Client/_Moffstation/Antags/UI/AntagEntry.xaml.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Content.Shared.Roles;
88
using Robust.Client.AutoGenerated;
99
using Robust.Client.UserInterface.Controls;
10+
using Robust.Client.UserInterface.CustomControls;
1011
using Robust.Client.UserInterface.XAML;
1112
using Robust.Shared.Prototypes;
1213

@@ -63,9 +64,19 @@ private void SetupRequirements(HumanoidCharacterProfile? profile) // Starlight
6364
AntagCheckbox.Visible = !locked;
6465
AntagCheckbox.Disabled = locked;
6566
LockIcon.Visible = locked;
66-
LockIcon.TooltipSupplier = reason != null
67+
#region Starlight
68+
/*LockIcon.TooltipSupplier = reason != null
6769
? _ => new RichTextLabel { Text = reason.ToString() }
68-
: null;
70+
: null;*/
71+
72+
if (!reason.IsEmpty)
73+
{
74+
var tooltip = new Tooltip();
75+
tooltip.SetMessage(reason);
76+
AntagCheckbox.TooltipSupplier = _ => tooltip;
77+
LockIcon.TooltipSupplier = _ => tooltip;
78+
}
79+
#endregion
6980

7081
if (locked)
7182
{
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#nullable enable
2+
using System.Linq;
3+
using Content.IntegrationTests.Fixtures;
4+
using Content.Server.GameTicking;
5+
using Content.Server.GameTicking.Rules;
6+
using Content.Shared.GameTicking.Rules;
7+
using Robust.Shared.GameObjects;
8+
using Robust.Shared.Prototypes;
9+
10+
namespace Content.IntegrationTests.Tests._Starlight.GameRules;
11+
12+
[TestFixture]
13+
[TestOf(typeof(DynamicRuleSystem))]
14+
public sealed class DynamicRuleTest : GameTest
15+
{
16+
private const string SequentialBudgetRule = "TestDynamicSequentialBudget";
17+
private const string MutualExclusionRule = "TestDynamicMutualExclusion";
18+
private const string CooldownRule = "TestDynamicCooldown";
19+
private const string CooldownChildRule = "TestDynamicCooldownChild";
20+
21+
[TestPrototypes]
22+
private const string Prototypes = @"
23+
- type: entity
24+
id: TestDynamicSequentialBudget
25+
parent: BaseGameRule
26+
components:
27+
- type: GameRule
28+
minPlayers: 0
29+
- type: DynamicRule
30+
startingBudgetMin: 325
31+
startingBudgetMax: 326
32+
budgetPerSecond: 0
33+
variantBudgetPerSecond: 0
34+
minRuleInterval: 86400
35+
maxRuleInterval: 86401
36+
table: !type:AllSelector
37+
children:
38+
- id: TestDynamicBudgetFirst
39+
conditions:
40+
- !type:HasBudgetCondition
41+
- id: TestDynamicBudgetSecond
42+
conditions:
43+
- !type:HasBudgetCondition
44+
45+
- type: entity
46+
id: TestDynamicBudgetFirst
47+
parent: BaseGameRule
48+
components:
49+
- type: DynamicRuleCost
50+
cost: 125
51+
52+
- type: entity
53+
id: TestDynamicBudgetSecond
54+
parent: BaseGameRule
55+
components:
56+
- type: DynamicRuleCost
57+
cost: 300
58+
59+
- type: entity
60+
id: TestDynamicMutualExclusion
61+
parent: BaseGameRule
62+
components:
63+
- type: GameRule
64+
minPlayers: 0
65+
- type: DynamicRule
66+
startingBudgetMin: 100
67+
startingBudgetMax: 101
68+
budgetPerSecond: 0
69+
variantBudgetPerSecond: 0
70+
minRuleInterval: 86400
71+
maxRuleInterval: 86401
72+
table: !type:AllSelector
73+
children:
74+
- id: TestDynamicExclusiveFirst
75+
conditions:
76+
- !type:HasBudgetCondition
77+
- id: TestDynamicExclusiveSecond
78+
conditions:
79+
- !type:HasBudgetCondition
80+
- !type:MutuallyExclusiveRuleCondition
81+
rules:
82+
- TestDynamicExclusiveFirst
83+
- id: TestDynamicExclusiveFirst
84+
conditions:
85+
- !type:HasBudgetCondition
86+
- !type:MaxRuleOccurenceCondition
87+
88+
- type: entity
89+
id: TestDynamicExclusiveFirst
90+
parent: BaseGameRule
91+
components:
92+
- type: GameRule
93+
delay:
94+
min: 60
95+
max: 60
96+
- type: DynamicRuleCost
97+
cost: 10
98+
99+
- type: entity
100+
id: TestDynamicExclusiveSecond
101+
parent: BaseGameRule
102+
components:
103+
- type: DynamicRuleCost
104+
cost: 10
105+
106+
- type: entity
107+
id: TestDynamicCooldown
108+
parent: BaseGameRule
109+
components:
110+
- type: GameRule
111+
minPlayers: 0
112+
- type: DynamicRule
113+
startingBudgetMin: 100
114+
startingBudgetMax: 101
115+
budgetPerSecond: 0
116+
variantBudgetPerSecond: 0
117+
minRuleInterval: 86400
118+
maxRuleInterval: 86401
119+
table: !type:AllSelector
120+
children:
121+
- id: TestDynamicCooldownChild
122+
conditions:
123+
- !type:HasBudgetCondition
124+
125+
- type: entity
126+
id: TestDynamicCooldownChild
127+
parent: BaseGameRule
128+
components:
129+
- type: DynamicRuleCost
130+
cost: 10
131+
cooldown: 1
132+
";
133+
134+
public override PoolSettings PoolSettings => new()
135+
{
136+
Dirty = true,
137+
DummyTicker = false,
138+
Connected = true,
139+
InLobby = true,
140+
};
141+
142+
/// <summary>
143+
/// Tests that the budget is updated between table children, so that a child rule can be selected and then the next child rule can be selected in the same table roll.
144+
/// </summary>
145+
[Test]
146+
public async Task DynamicBudgetUpdateTest()
147+
{
148+
var server = Pair.Server;
149+
150+
await server.WaitAssertion(() =>
151+
{
152+
var ticker = server.System<GameTicker>();
153+
var uid = ticker.AddGameRule(SequentialBudgetRule);
154+
var component = server.EntMan.GetComponent<DynamicRuleComponent>(uid);
155+
156+
Assert.Multiple(() =>
157+
{
158+
Assert.That(component.Budget, Is.EqualTo(200));
159+
Assert.That(component.Rules, Has.Count.EqualTo(1));
160+
Assert.That(GetPrototypeId(server.EntMan, component.Rules.Single()),
161+
Is.EqualTo("TestDynamicBudgetFirst"));
162+
});
163+
164+
ticker.EndGameRule(uid);
165+
});
166+
}
167+
168+
/// <summary>
169+
/// Tests that mutually exclusive rules are rejected within the same table roll, so that only one of the mutually exclusive rules is selected.
170+
/// </summary>
171+
/// <returns></returns>
172+
[Test]
173+
public async Task DynamicMutuallyExclusiveRulesRejectionTest()
174+
{
175+
var server = Pair.Server;
176+
177+
await server.WaitAssertion(() =>
178+
{
179+
var ticker = server.System<GameTicker>();
180+
var uid = ticker.AddGameRule(MutualExclusionRule);
181+
var component = server.EntMan.GetComponent<DynamicRuleComponent>(uid);
182+
183+
Assert.Multiple(() =>
184+
{
185+
Assert.That(component.Budget, Is.EqualTo(90));
186+
Assert.That(component.Rules, Has.Count.EqualTo(1));
187+
Assert.That(GetPrototypeId(server.EntMan, component.Rules.Single()),
188+
Is.EqualTo("TestDynamicExclusiveFirst"));
189+
});
190+
191+
ticker.EndGameRule(uid);
192+
});
193+
}
194+
195+
/// <summary>
196+
/// Tests that a rule with a cooldown is not selected in the next Dynamic round after being selected in the previous Dynamic round.
197+
/// </summary>
198+
[Test]
199+
public async Task DynamicRuleCooldownTest()
200+
{
201+
var server = Pair.Server;
202+
var ticker = server.System<GameTicker>();
203+
var dynamic = server.System<DynamicRuleSystem>();
204+
205+
await server.WaitAssertion(() =>
206+
{
207+
var uid = ticker.AddGameRule(CooldownRule);
208+
var component = server.EntMan.GetComponent<DynamicRuleComponent>(uid);
209+
210+
Assert.Multiple(() =>
211+
{
212+
Assert.That(component.Rules, Has.Count.EqualTo(1));
213+
Assert.That(GetPrototypeId(server.EntMan, component.Rules.Single()),
214+
Is.EqualTo(CooldownChildRule));
215+
});
216+
217+
// A cooldown applies to future Dynamic rounds, not later rolls in the current round.
218+
Assert.That(dynamic.ExecuteNow(uid).Count(), Is.EqualTo(1));
219+
Assert.That(component.Rules, Has.Count.EqualTo(2));
220+
});
221+
222+
await server.WaitPost(() => ticker.RestartRound());
223+
await Pair.RunUntilSynced();
224+
225+
await server.WaitAssertion(() =>
226+
{
227+
var uid = ticker.AddGameRule(CooldownRule);
228+
var component = server.EntMan.GetComponent<DynamicRuleComponent>(uid);
229+
230+
Assert.Multiple(() =>
231+
{
232+
Assert.That(component.Rules, Is.Empty);
233+
Assert.That(component.Budget, Is.EqualTo(100));
234+
});
235+
236+
// The snapshot remains in force for every roll during this Dynamic round.
237+
Assert.That(dynamic.ExecuteNow(uid), Is.Empty);
238+
});
239+
240+
await server.WaitPost(() => ticker.RestartRound());
241+
await Pair.RunUntilSynced();
242+
243+
await server.WaitAssertion(() =>
244+
{
245+
var uid = ticker.AddGameRule(CooldownRule);
246+
var component = server.EntMan.GetComponent<DynamicRuleComponent>(uid);
247+
248+
Assert.That(component.Rules, Has.Count.EqualTo(1));
249+
Assert.That(GetPrototypeId(server.EntMan, component.Rules.Single()),
250+
Is.EqualTo(CooldownChildRule));
251+
});
252+
}
253+
254+
private static string? GetPrototypeId(IEntityManager entityManager, EntityUid uid) => entityManager.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID;
255+
}

Content.IntegrationTests/Tests/_Starlight/Round/CharacterSelectionTest.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ public sealed class CharacterSelectionTest : GameTest
8484
private static readonly ProtoId<JobPrototype> Clown = "Clown";
8585
private static readonly ProtoId<AntagPrototype> Traitor = "Traitor";
8686
private static readonly ProtoId<AntagSpecifierPrototype> TraitorSpecifier = "Traitor";
87+
private static readonly ProtoId<AntagPrototype> InitialInfected = "InitialInfected";
88+
private static readonly ProtoId<AntagSpecifierPrototype> InitialInfectedSpecifier = "InitialInfected";
8789

8890
// helper structs for test case definition readability
8991
public sealed class TestCharacter
@@ -296,6 +298,22 @@ public Dictionary<ProtoId<JobPrototype>, JobPriority> MakeJobPrioDict()
296298
],
297299
ExpectedJob = Mime,
298300
ExpectTraitor = true
301+
},
302+
// Antag eligibility must not allow the final profile picker to choose
303+
// a different character with the same job but without the selected antag enabled.
304+
// Basically this tries to catch a bug where you select a character with a job and antag enabled, but then
305+
// the final profilepicker chooses a different character with the same job but without the antag enabled.
306+
new()
307+
{
308+
Description = "Same job, one antag character",
309+
HighPrioJob = Passenger,
310+
Characters =
311+
[
312+
new() { Jobs = [ Passenger ], IsTraitor = true, ExpectToSpawn = true },
313+
new() { Jobs = [ Passenger ] }
314+
],
315+
ExpectedJob = Passenger,
316+
ExpectTraitor = true
299317
}
300318
];
301319

@@ -414,6 +432,21 @@ await pair.Server.WaitPost(() =>
414432
});
415433
}
416434

435+
// Test that a character with a profile that is not valid for a specific antag cannot be selected as that antag
436+
// It really just exists to see if IPCs are rolling Initial Infected...
437+
[Test]
438+
public Task ProfileSpecificAntagRequirementsTest() =>
439+
Pair.Server.WaitAssertion(() =>
440+
{
441+
var antagSystem = Pair.Server.System<AntagSelectionSystem>();
442+
var definition = Pair.Server.ProtoMan.Index(InitialInfectedSpecifier);
443+
var human = HumanoidCharacterProfile.RandomWithSpecies("Human").AsEnabled().WithAntagPreferences([InitialInfected]);
444+
var ipc = HumanoidCharacterProfile.RandomWithSpecies("IPC").AsEnabled().WithAntagPreferences([InitialInfected]);
445+
446+
Assert.That(antagSystem.IsProfileValidForAntag(Pair.Player!, human, definition), Is.True);
447+
Assert.That(antagSystem.IsProfileValidForAntag(Pair.Player!, ipc, definition), Is.False);
448+
});
449+
417450
// Run multiple round starts with the same set of characters, all of which are valid to select,
418451
// and verify that which character is selected varies
419452
// This uses a random seed and theoretically has a chance of randomly failing naturally.

0 commit comments

Comments
 (0)