Skip to content

Commit 67b8d4b

Browse files
Add support for funny messages in game server configuration
- Implemented functionality to populate and validate funny messages in GameServersController. - Added new view model properties for funny messages in GameServerEditViewModel and GlobalSettingsViewModel. - Created validation rules for funny messages to ensure they do not exceed 120 characters. - Developed new views for managing funny messages in the game server configuration. - Updated existing tests and added new tests to cover funny message validation and configuration.
1 parent c3c6bb0 commit 67b8d4b

11 files changed

Lines changed: 695 additions & 48 deletions

File tree

src/XtremeIdiots.Portal.Web.Tests/Controllers/GameServersControllerTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,37 @@ public void PopulateConfigFromNamespace_BroadcastsNamespace_ParsesStringBooleans
173173
Assert.False(model.BroadcastMessages[1].Enabled);
174174
}
175175

176+
[Fact]
177+
public void PopulateConfigFromNamespace_FunnyMessagesNamespace_MapsAllFields()
178+
{
179+
// Arrange
180+
var sut = CreateSut();
181+
var method = GetPrivateInstanceMethod("PopulateConfigFromNamespace");
182+
var model = new GameServerEditViewModel();
183+
var config = JsonConvert.DeserializeObject<ConfigurationDto>(JsonConvert.SerializeObject(new
184+
{
185+
Namespace = "funnyMessages",
186+
Configuration = """
187+
{
188+
"messages": [
189+
{ "message": "^1FU^7 {name}", "enabled": true },
190+
{ "message": "{name} got owned", "enabled": false }
191+
]
192+
}
193+
"""
194+
}));
195+
196+
// Act
197+
method.Invoke(sut, [model, config]);
198+
199+
// Assert
200+
Assert.Equal(2, model.FunnyMessages.Count);
201+
Assert.Equal("^1FU^7 {name}", model.FunnyMessages[0].Message);
202+
Assert.True(model.FunnyMessages[0].Enabled);
203+
Assert.Equal("{name} got owned", model.FunnyMessages[1].Message);
204+
Assert.False(model.FunnyMessages[1].Enabled);
205+
}
206+
176207
[Fact]
177208
public void PopulateConfigFromNamespace_ParsesStringBooleans_ForOtherConfigNamespaces()
178209
{
@@ -252,6 +283,11 @@ public async Task SaveConfigNamespacesAsync_AgentEnabled_UpsertsBroadcastsContra
252283
[
253284
new BroadcastMessageViewModel { Message = "^1Welcome", Enabled = true },
254285
new BroadcastMessageViewModel { Message = "^2Rules", Enabled = false }
286+
],
287+
FunnyMessages =
288+
[
289+
new BroadcastMessageViewModel { Message = "^5FU {name}", Enabled = true },
290+
new BroadcastMessageViewModel { Message = "{name} is unlucky", Enabled = false }
255291
]
256292
};
257293

@@ -269,6 +305,16 @@ public async Task SaveConfigNamespacesAsync_AgentEnabled_UpsertsBroadcastsContra
269305
Assert.Equal(2, root.GetProperty("messages").GetArrayLength());
270306
Assert.False(root.GetProperty("messages")[1].GetProperty("enabled").GetBoolean());
271307
Assert.Equal("^2Rules", root.GetProperty("messages")[1].GetProperty("message").GetString());
308+
309+
Assert.True(upsertPayloads.TryGetValue("funnyMessages", out var funnyMessagesJson));
310+
using var funnyDoc = System.Text.Json.JsonDocument.Parse(funnyMessagesJson);
311+
var funnyRoot = funnyDoc.RootElement;
312+
313+
Assert.Equal(2, funnyRoot.GetProperty("messages").GetArrayLength());
314+
Assert.Equal("^5FU {name}", funnyRoot.GetProperty("messages")[0].GetProperty("message").GetString());
315+
Assert.True(funnyRoot.GetProperty("messages")[0].GetProperty("enabled").GetBoolean());
316+
Assert.Equal("{name} is unlucky", funnyRoot.GetProperty("messages")[1].GetProperty("message").GetString());
317+
Assert.False(funnyRoot.GetProperty("messages")[1].GetProperty("enabled").GetBoolean());
272318
}
273319

274320
private static MethodInfo GetPrivateInstanceMethod(string name)

src/XtremeIdiots.Portal.Web.Tests/ViewModels/GameServerEditViewModelTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,61 @@ public void Validate_WhenBroadcastMessagesIsNull_IsValid()
6060
Assert.True(isValid);
6161
}
6262

63+
[Fact]
64+
public void Validate_WhenFunnyMessageExceeds120Characters_ReturnsValidationError()
65+
{
66+
var model = CreateValidModel();
67+
model.FunnyMessages =
68+
[
69+
new BroadcastMessageViewModel
70+
{
71+
Message = new string('a', 121),
72+
Enabled = true
73+
}
74+
];
75+
76+
var isValid = Validator.TryValidateObject(model, new ValidationContext(model), [], true);
77+
78+
Assert.False(isValid);
79+
}
80+
81+
[Fact]
82+
public void Validate_WhenFunnyMessageIs120Characters_IsValid()
83+
{
84+
var model = CreateValidModel();
85+
model.FunnyMessages =
86+
[
87+
new BroadcastMessageViewModel
88+
{
89+
Message = new string('a', 120),
90+
Enabled = true
91+
}
92+
];
93+
94+
var isValid = Validator.TryValidateObject(model, new ValidationContext(model), [], true);
95+
96+
Assert.True(isValid);
97+
}
98+
99+
[Fact]
100+
public void Validate_WhenBroadcastMessagesIsNull_StillValidatesFunnyMessages()
101+
{
102+
var model = CreateValidModel();
103+
model.BroadcastMessages = null!;
104+
model.FunnyMessages =
105+
[
106+
new BroadcastMessageViewModel
107+
{
108+
Message = new string('a', 121),
109+
Enabled = true
110+
}
111+
];
112+
113+
var isValid = Validator.TryValidateObject(model, new ValidationContext(model), [], true);
114+
115+
Assert.False(isValid);
116+
}
117+
63118
private static GameServerEditViewModel CreateValidModel()
64119
{
65120
return new GameServerEditViewModel
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using XtremeIdiots.Portal.Web.ViewModels;
3+
4+
namespace XtremeIdiots.Portal.Web.Tests.ViewModels;
5+
6+
public class GlobalSettingsViewModelTests
7+
{
8+
[Fact]
9+
public void Validate_WhenFunnyMessageExceeds120Characters_ReturnsValidationError()
10+
{
11+
var model = new GlobalSettingsViewModel
12+
{
13+
FunnyMessages =
14+
[
15+
new BroadcastMessageViewModel
16+
{
17+
Message = new string('a', 121),
18+
Enabled = true
19+
}
20+
]
21+
};
22+
23+
var isValid = Validator.TryValidateObject(model, new ValidationContext(model), [], true);
24+
25+
Assert.False(isValid);
26+
}
27+
28+
[Fact]
29+
public void Validate_WhenFunnyMessageIs120Characters_IsValid()
30+
{
31+
var model = new GlobalSettingsViewModel
32+
{
33+
FunnyMessages =
34+
[
35+
new BroadcastMessageViewModel
36+
{
37+
Message = new string('a', 120),
38+
Enabled = true
39+
}
40+
]
41+
};
42+
43+
var isValid = Validator.TryValidateObject(model, new ValidationContext(model), [], true);
44+
45+
Assert.True(isValid);
46+
}
47+
}

src/XtremeIdiots.Portal.Web/Controllers/GameServersController.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,9 @@ private void PopulateConfigFromNamespace(GameServerEditViewModel editModel, Conf
671671
editModel.BroadcastsIntervalSeconds = GetNullableIntProperty(root, "intervalSeconds") ?? GameServerEditViewModel.DefaultBroadcastIntervalSeconds;
672672
editModel.BroadcastMessages = GetBroadcastMessages(root);
673673
break;
674+
case "funnyMessages":
675+
editModel.FunnyMessages = GetBroadcastMessages(root);
676+
break;
674677
default:
675678
Logger.LogDebug("Unknown configuration namespace '{Namespace}' for game server", config.Namespace);
676679
break;
@@ -779,6 +782,9 @@ private void PopulateGlobalDefaults(GameServerEditViewModel editModel, Configura
779782
editModel.GlobalEventsStaleThresholdSeconds = GetIntProperty(root, "staleThresholdSeconds", editModel.GlobalEventsStaleThresholdSeconds);
780783
editModel.GlobalEventsPlayerCacheExpirationSeconds = GetIntProperty(root, "playerCacheExpirationSeconds", editModel.GlobalEventsPlayerCacheExpirationSeconds);
781784
break;
785+
case "funnyMessages":
786+
editModel.GlobalFunnyMessages = GetBroadcastMessages(root);
787+
break;
782788
default:
783789
break;
784790
}
@@ -1016,6 +1022,18 @@ await UpsertConfigSafeAsync(gameServerId, "events",
10161022

10171023
await UpsertConfigSafeAsync(gameServerId, "broadcasts",
10181024
JsonSerializer.Serialize(broadcastsConfig, configJsonOptions), serverTitle, errors, cancellationToken).ConfigureAwait(false);
1025+
1026+
var funnyMessagesConfig = new
1027+
{
1028+
messages = (model.FunnyMessages ?? []).Select(m => new
1029+
{
1030+
message = m.Message,
1031+
enabled = m.Enabled
1032+
})
1033+
};
1034+
1035+
await UpsertConfigSafeAsync(gameServerId, "funnyMessages",
1036+
JsonSerializer.Serialize(funnyMessagesConfig, configJsonOptions), serverTitle, errors, cancellationToken).ConfigureAwait(false);
10191037
}
10201038
}
10211039

src/XtremeIdiots.Portal.Web/Controllers/GlobalSettingsController.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ await UpsertConfigSafeAsync("events", JsonSerializer.Serialize(new
103103
playerCacheExpirationSeconds = model.EventsPlayerCacheExpirationSeconds
104104
}, configJsonOptions), errors, cancellationToken).ConfigureAwait(false);
105105

106+
await UpsertConfigSafeAsync("funnyMessages", JsonSerializer.Serialize(new
107+
{
108+
messages = (model.FunnyMessages ?? []).Select(m => new
109+
{
110+
message = m.Message,
111+
enabled = m.Enabled
112+
})
113+
}, configJsonOptions), errors, cancellationToken).ConfigureAwait(false);
114+
106115
if (errors.Count > 0)
107116
{
108117
this.AddAlertDanger($"Failed to save configuration for: {string.Join(", ", errors)}");
@@ -151,6 +160,9 @@ private void PopulateModelFromNamespace(GlobalSettingsViewModel model, Configura
151160
model.EventsStaleThresholdSeconds = GetIntProperty(root, "staleThresholdSeconds", model.EventsStaleThresholdSeconds);
152161
model.EventsPlayerCacheExpirationSeconds = GetIntProperty(root, "playerCacheExpirationSeconds", model.EventsPlayerCacheExpirationSeconds);
153162
break;
163+
case "funnyMessages":
164+
model.FunnyMessages = GetMessageTemplates(root);
165+
break;
154166
default:
155167
Logger.LogDebug("Unknown global configuration namespace '{Namespace}'", config.Namespace);
156168
break;
@@ -194,6 +206,45 @@ private static string NormalizeAgentName(string? value)
194206
: null;
195207
}
196208

209+
private static bool GetBoolProperty(JsonElement root, string propertyName, bool defaultValue)
210+
{
211+
if (root.TryGetProperty(propertyName, out var prop))
212+
{
213+
if (prop.ValueKind == JsonValueKind.True)
214+
return true;
215+
216+
if (prop.ValueKind == JsonValueKind.False)
217+
return false;
218+
219+
if (prop.ValueKind == JsonValueKind.String && bool.TryParse(prop.GetString(), out var parsedBool))
220+
return parsedBool;
221+
}
222+
223+
return defaultValue;
224+
}
225+
226+
private static List<BroadcastMessageViewModel> GetMessageTemplates(JsonElement root)
227+
{
228+
var messages = new List<BroadcastMessageViewModel>();
229+
230+
if (!root.TryGetProperty("messages", out var messagesElement) || messagesElement.ValueKind != JsonValueKind.Array)
231+
return messages;
232+
233+
foreach (var item in messagesElement.EnumerateArray())
234+
{
235+
if (item.ValueKind != JsonValueKind.Object)
236+
continue;
237+
238+
messages.Add(new BroadcastMessageViewModel
239+
{
240+
Message = GetStringProperty(item, "message") ?? string.Empty,
241+
Enabled = GetBoolProperty(item, "enabled", true)
242+
});
243+
}
244+
245+
return messages;
246+
}
247+
197248
private async Task UpsertConfigSafeAsync(
198249
string ns,
199250
string configJson,

src/XtremeIdiots.Portal.Web/ViewModels/GameServerEditViewModel.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class GameServerEditViewModel : IValidatableObject
1212
{
1313
public const int DefaultBroadcastIntervalSeconds = 500;
1414
public const int MaxBroadcastMessageLength = 120;
15+
public const int MaxFunnyMessageLength = 120;
1516

1617
/// <summary>
1718
/// Core game server data
@@ -110,6 +111,14 @@ public class GameServerEditViewModel : IValidatableObject
110111

111112
public List<BroadcastMessageViewModel> BroadcastMessages { get; set; } = [];
112113

114+
// Funny messages configuration (parsed from "funnyMessages" config namespace)
115+
116+
public List<BroadcastMessageViewModel> FunnyMessages { get; set; } = [];
117+
118+
// Global funny message defaults used for server-level inheritance awareness
119+
120+
public List<BroadcastMessageViewModel> GlobalFunnyMessages { get; set; } = [];
121+
113122
// Global defaults (for placeholder display in override fields)
114123

115124
public int GlobalModerationHateSeverityThreshold { get; set; } = GlobalSettingsViewModel.DisabledSeverityThreshold;
@@ -160,13 +169,22 @@ public static int GetDefaultPort(RepoFileTransportType fileTransportType)
160169

161170
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
162171
{
163-
if (BroadcastMessages is null)
164-
yield break;
172+
if (BroadcastMessages is not null)
173+
{
174+
for (var i = 0; i < BroadcastMessages.Count; i++)
175+
{
176+
if (BroadcastMessages[i].Message?.Length > MaxBroadcastMessageLength)
177+
yield return new ValidationResult($"Broadcast message cannot exceed {MaxBroadcastMessageLength} characters.", [$"BroadcastMessages[{i}].Message"]);
178+
}
179+
}
165180

166-
for (var i = 0; i < BroadcastMessages.Count; i++)
181+
if (FunnyMessages is not null)
167182
{
168-
if (BroadcastMessages[i].Message?.Length > MaxBroadcastMessageLength)
169-
yield return new ValidationResult($"Broadcast message cannot exceed {MaxBroadcastMessageLength} characters.", [$"BroadcastMessages[{i}].Message"]);
183+
for (var i = 0; i < FunnyMessages.Count; i++)
184+
{
185+
if (FunnyMessages[i].Message?.Length > MaxFunnyMessageLength)
186+
yield return new ValidationResult($"Funny message cannot exceed {MaxFunnyMessageLength} characters.", [$"FunnyMessages[{i}].Message"]);
187+
}
170188
}
171189
}
172190
}

src/XtremeIdiots.Portal.Web/ViewModels/GlobalSettingsViewModel.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ namespace XtremeIdiots.Portal.Web.ViewModels;
77
/// <summary>
88
/// View model for the global settings admin page, representing fleet-wide configuration defaults
99
/// </summary>
10-
public class GlobalSettingsViewModel
10+
public class GlobalSettingsViewModel : IValidatableObject
1111
{
1212
public const int DisabledSeverityThreshold = -1;
1313
public const string DefaultAgentName = "^4[^1>XI< BOT^4]^7";
14+
public const int MaxFunnyMessageLength = 120;
1415

1516
// Agent defaults
1617
[DisplayName("Poll Interval (ms)")]
@@ -68,6 +69,22 @@ public class GlobalSettingsViewModel
6869
[Range(1, int.MaxValue, ErrorMessage = "Player cache expiration must be at least 1 second.")]
6970
public int EventsPlayerCacheExpirationSeconds { get; set; } = 900;
7071

72+
// Global funny messages defaults
73+
74+
public List<BroadcastMessageViewModel> FunnyMessages { get; set; } = [];
75+
76+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
77+
{
78+
if (FunnyMessages is null)
79+
yield break;
80+
81+
for (var i = 0; i < FunnyMessages.Count; i++)
82+
{
83+
if (FunnyMessages[i].Message?.Length > MaxFunnyMessageLength)
84+
yield return new ValidationResult($"Funny message cannot exceed {MaxFunnyMessageLength} characters.", [$"FunnyMessages[{i}].Message"]);
85+
}
86+
}
87+
7188
public static IReadOnlyList<SelectListItem> BuildSeverityOptions(bool includeUseGlobalOption)
7289
{
7390
var options = new List<SelectListItem>();

src/XtremeIdiots.Portal.Web/Views/GameServers/ConfigurationSections/_BroadcastsConfiguration.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<div id="broadcast-messages-container">
4040
@for (var i = 0; i < broadcastMessages.Count; i++)
4141
{
42-
<div class="border rounded p-3 mb-3" data-broadcast-row>
42+
<div class="border rounded p-3 mb-3" data-message-row="broadcast">
4343
<div class="d-flex justify-content-between align-items-center mb-2">
4444
<div class="form-check form-switch mb-0">
4545
<input type="hidden" data-field="enabled-hidden" name="BroadcastMessages[@i].Enabled" value="false" />
@@ -76,7 +76,7 @@
7676
</div>
7777

7878
<template id="broadcast-message-template">
79-
<div class="border rounded p-3 mb-3" data-broadcast-row>
79+
<div class="border rounded p-3 mb-3" data-message-row="broadcast">
8080
<div class="d-flex justify-content-between align-items-center mb-2">
8181
<div class="form-check form-switch mb-0">
8282
<input type="hidden" data-field="enabled-hidden" value="false" />

0 commit comments

Comments
 (0)