Skip to content

Commit 4b08675

Browse files
committed
feat: show active players vote selection, total votes and option to hide reminder if already voted
1 parent 59f2e3c commit 4b08675

9 files changed

Lines changed: 298 additions & 14 deletions

File tree

Configuration/ConfigService.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class ConfigService : IEagerService
1616
public ConfigEntry<bool> SubmitRecords { get; private set; }
1717
public ConfigEntry<bool> ShowRecordSubmitMessage { get; private set; }
1818
public ConfigEntry<float> ShowRecordSubmitMessageDuration { get; private set; }
19+
public ConfigEntry<bool> ShowVoteReminderAfterVoting { get; private set; }
1920

2021
public ConfigEntry<bool> EnableGhosts { get; private set; }
2122

@@ -82,6 +83,7 @@ public ConfigService(ConfigFile config, Plugin plugin)
8283
ConfigDiscord(config);
8384
ConfigUrls(config);
8485
ConfigPlayback(config);
86+
ConfigChatMessages(config);
8587

8688
SettingsApi.ConfigureModSettingsTabs(plugin, builder =>
8789
{
@@ -96,6 +98,8 @@ public ConfigService(ConfigFile config, Plugin plugin)
9698
"3. Record Holder - General",
9799
"3.1 Record Holder - Visibility",
98100
"3.2 Record Holder - Keys");
101+
builder.Tab("Chat Messages",
102+
"7. Chat Messages");
99103
builder.Tab("Other",
100104
"4. Discord",
101105
"5. URLs");
@@ -310,6 +314,15 @@ private void ConfigUrls(ConfigFile config)
310314
"Use http://127.0.0.1:5000/ instead of production GraphQL");
311315
}
312316

317+
private void ConfigChatMessages(ConfigFile config)
318+
{
319+
ShowVoteReminderAfterVoting = config.Bind(
320+
"7. Chat Messages",
321+
"1. Show Vote Reminder After Voting",
322+
true,
323+
"Should the vote reminder be shown when you have already voted on the current level");
324+
}
325+
313326
private void ConfigPlayback(ConfigFile config)
314327
{
315328
ShowTimeline = config.Bind(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
query GetLevelVoteSummary($xxHash: String!, $steamId: BigInt!) {
2+
voteCounts: votes(filter: { level: { xxHash: { equalTo: $xxHash } } }) {
3+
groupedAggregates(groupBy: [VALUE]) {
4+
keys
5+
distinctCount {
6+
userId
7+
}
8+
}
9+
}
10+
currentVote: votes(
11+
first: 1
12+
filter: {
13+
level: { xxHash: { equalTo: $xxHash } }
14+
user: { steamId: { equalTo: $steamId } }
15+
}
16+
) {
17+
nodes {
18+
value
19+
}
20+
}
21+
}

Plugin.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ private void ConfigureServices(IServiceCollection services)
115115
services.AddEagerService<DiscordService>();
116116
services.AddEagerService<UnhandledExceptionLoggerService>();
117117
services.AddEagerService<VotingService>();
118+
services.AddSingleton<VotingGraphqlService>();
118119
services.AddSingleton<AssetService>();
119120
services.AddSingleton<GhostReaderFactory>();
120121
services.AddSingleton<GhostRecorderFactory>();

Voting/VoteReminderFormatter.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Globalization;
2+
3+
namespace TNRD.Zeepkist.GTR.Voting;
4+
5+
public static class VoteReminderFormatter
6+
{
7+
public static bool ShouldShow(bool showAfterVoting, VoteSummary summary)
8+
{
9+
return showAfterVoting || !summary.CurrentVote.HasValue;
10+
}
11+
12+
public static string Format(VoteSummary summary)
13+
{
14+
return
15+
"<size=80%><color=#FFFF00>Cast your vote for ZeepCentraal:</color></size><br>" +
16+
"<size=75%>" +
17+
"<size=50%><i>(hated it)</i></size> " +
18+
FormatChoice(summary, -2, "--", "#FF0000") + " " +
19+
FormatChoice(summary, -1, "-", "#FF8000") + " " +
20+
FormatChoice(summary, 0, "-+/+-", "#FFFF00") + " " +
21+
FormatChoice(summary, 1, "+", "#80FF00") + " " +
22+
FormatChoice(summary, 2, "++", "#00FF00") + " " +
23+
"<size=50%><i>(loved it)</i></size>" +
24+
"</size>";
25+
}
26+
27+
private static string FormatChoice(VoteSummary summary, int voteValue, string label, string colour)
28+
{
29+
string choice = $"<b><color={colour}>{label}</color></b>";
30+
if (summary.CurrentVote == voteValue)
31+
choice = $"[{choice}]";
32+
33+
long? count = summary.GetCount(voteValue);
34+
string countText = count?.ToString(CultureInfo.InvariantCulture) ?? "?";
35+
return $"{choice} <size=50%>({countText})</size>";
36+
}
37+
}

Voting/VoteSummary.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Generic;
2+
3+
namespace TNRD.Zeepkist.GTR.Voting;
4+
5+
public class VoteSummary
6+
{
7+
private readonly IReadOnlyDictionary<int, long> _counts;
8+
9+
public static VoteSummary Unknown { get; } = new(null, null);
10+
11+
public int? CurrentVote { get; }
12+
public bool CountsKnown => _counts != null;
13+
14+
public VoteSummary(IReadOnlyDictionary<int, long> counts, int? currentVote)
15+
{
16+
_counts = counts;
17+
CurrentVote = currentVote is >= -2 and <= 2 ? currentVote : null;
18+
}
19+
20+
public long? GetCount(int voteValue)
21+
{
22+
if (_counts == null)
23+
return null;
24+
25+
return _counts.TryGetValue(voteValue, out long count) ? count : 0;
26+
}
27+
}

Voting/VotingGraphqlService.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Linq;
5+
using System.Threading;
6+
using StrawberryShake;
7+
using ZeepSDK.External.Cysharp.Threading.Tasks;
8+
using ZeepSDK.External.FluentResults;
9+
10+
namespace TNRD.Zeepkist.GTR.Voting;
11+
12+
public class VotingGraphqlService
13+
{
14+
private readonly IGtrClient _gtrClient;
15+
16+
public VotingGraphqlService(IGtrClient gtrClient)
17+
{
18+
_gtrClient = gtrClient;
19+
}
20+
21+
public async UniTask<Result<VoteSummary>> GetVoteSummary(string xxHash, ulong steamId,
22+
CancellationToken cancellationToken)
23+
{
24+
try
25+
{
26+
IOperationResult<IGetLevelVoteSummaryResult> result =
27+
await _gtrClient.GetLevelVoteSummary.ExecuteAsync(
28+
xxHash,
29+
steamId.ToString(CultureInfo.InvariantCulture),
30+
cancellationToken);
31+
result.EnsureNoErrors();
32+
33+
var counts = new Dictionary<int, long>
34+
{
35+
[-2] = 0,
36+
[-1] = 0,
37+
[0] = 0,
38+
[1] = 0,
39+
[2] = 0
40+
};
41+
42+
IReadOnlyList<IGetLevelVoteSummary_VoteCounts_GroupedAggregates> groupedAggregates =
43+
result.Data.VoteCounts?.GroupedAggregates;
44+
if (groupedAggregates != null)
45+
{
46+
foreach (IGetLevelVoteSummary_VoteCounts_GroupedAggregates aggregate in groupedAggregates)
47+
{
48+
string key = aggregate.Keys?.FirstOrDefault();
49+
string count = aggregate.DistinctCount?.UserId;
50+
if (int.TryParse(key, NumberStyles.Integer, CultureInfo.InvariantCulture, out int voteValue) &&
51+
voteValue is >= -2 and <= 2 &&
52+
long.TryParse(count, NumberStyles.Integer, CultureInfo.InvariantCulture, out long voteCount))
53+
{
54+
counts[voteValue] = voteCount;
55+
}
56+
}
57+
}
58+
59+
int? currentVote = result.Data.CurrentVote?.Nodes.FirstOrDefault()?.Value;
60+
return Result.Ok(new VoteSummary(counts, currentVote));
61+
}
62+
catch (Exception e)
63+
{
64+
return Result.Fail(new ExceptionalError(e));
65+
}
66+
}
67+
}

Voting/VotingService.cs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
using System;
22
using System.Net.Http;
3+
using System.Threading;
34
using JetBrains.Annotations;
45
using Microsoft.Extensions.Logging;
6+
using Steamworks;
57
using TNRD.Zeepkist.GTR.Api;
8+
using TNRD.Zeepkist.GTR.Configuration;
69
using TNRD.Zeepkist.GTR.Core;
710
using TNRD.Zeepkist.GTR.PlayerLoop;
811
using ZeepkistClient;
912
using ZeepSDK.Chat;
1013
using ZeepSDK.External.Cysharp.Threading.Tasks;
14+
using ZeepSDK.External.FluentResults;
1115
using ZeepSDK.Level;
1216
using ZeepSDK.Messaging;
1317
using ZeepSDK.Multiplayer;
@@ -23,43 +27,79 @@ public class VotingService : IEagerService
2327
private readonly PlayerLoopService _playerLoopService;
2428
private readonly ILogger<VotingService> _logger;
2529
private readonly ApiHttpClient _apiHttpClient;
30+
private readonly ConfigService _configService;
31+
private readonly VotingGraphqlService _votingGraphqlService;
2632

2733
private string _previousTimeLeft;
34+
private int _reminderRequestVersion;
2835

2936
public VotingService(PlayerLoopService playerLoopService, ILogger<VotingService> logger,
30-
ApiHttpClient apiHttpClient)
37+
ApiHttpClient apiHttpClient, ConfigService configService, VotingGraphqlService votingGraphqlService)
3138
{
3239
_playerLoopService = playerLoopService;
3340
_logger = logger;
3441
_apiHttpClient = apiHttpClient;
42+
_configService = configService;
43+
_votingGraphqlService = votingGraphqlService;
3544
_playerLoopService.SubscribeUpdate(OnUpdate);
3645
}
3746

3847
private void OnUpdate()
3948
{
4049
if (!MultiplayerApi.IsPlayingOnline)
50+
{
51+
if (_previousTimeLeft != null)
52+
{
53+
_previousTimeLeft = null;
54+
_reminderRequestVersion++;
55+
}
4156
return;
57+
}
4258

4359
string currentTimeLeft = ZeepkistNetwork.CurrentLobby.timeLeftString;
4460

4561
if (currentTimeLeft == TIME_LEFT && _previousTimeLeft != TIME_LEFT)
62+
ShowVoteReminderAsync(++_reminderRequestVersion).Forget();
63+
64+
_previousTimeLeft = currentTimeLeft;
65+
}
66+
67+
private async UniTaskVoid ShowVoteReminderAsync(int requestVersion)
68+
{
69+
string currentHash = LevelApi.CurrentHashV2?.Hash;
70+
VoteSummary summary = VoteSummary.Unknown;
71+
72+
if (string.IsNullOrEmpty(currentHash))
4673
{
47-
ChatApi.AddLocalMessage(
48-
"<size=80%><color=#FFFF00>Cast your vote for ZeepCentraal:</color></size><br>" +
49-
"<size=75%>" +
50-
"<size=50%><i>(hated it)</i></size> " +
51-
"<b><color=#FF0000>--</color></b> " +
52-
"<b><color=#FF8000>-</color></b> " +
53-
"<b><color=#FFFF00>-+</color>/<color=#FFFF00>+-</color></b> " +
54-
"<b><color=#80FF00>+</color></b> " +
55-
"<b><color=#00FF00>++</color></b> " +
56-
"<size=50%><i>(loved it)</i></size>" +
57-
"</size>"
58-
);
74+
_logger.LogError("Unable to get vote summary because current level hash is empty");
75+
}
76+
else
77+
{
78+
Result<VoteSummary> result = await _votingGraphqlService.GetVoteSummary(
79+
currentHash,
80+
SteamClient.SteamId.Value,
81+
CancellationToken.None);
82+
if (result.IsSuccess)
83+
{
84+
summary = result.Value;
85+
}
86+
else
87+
{
88+
_logger.LogError("Failed to get vote summary: {Result}", result);
89+
}
90+
}
5991

92+
if (requestVersion != _reminderRequestVersion ||
93+
!MultiplayerApi.IsPlayingOnline ||
94+
!string.Equals(currentHash, LevelApi.CurrentHashV2?.Hash, StringComparison.Ordinal))
95+
{
96+
return;
6097
}
6198

62-
_previousTimeLeft = currentTimeLeft;
99+
if (!VoteReminderFormatter.ShouldShow(_configService.ShowVoteReminderAfterVoting.Value, summary))
100+
return;
101+
102+
ChatApi.AddLocalMessage(VoteReminderFormatter.Format(summary));
63103
}
64104

65105
private void OnVoteSuccess(string vote)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using TNRD.Zeepkist.GTR.Voting;
2+
using Xunit;
3+
4+
namespace TNRD.Zeepkist.GTR.Tests;
5+
6+
public class VoteReminderFormatterTests
7+
{
8+
[Fact]
9+
public void FormatsCountsAndHighlightsCurrentVote()
10+
{
11+
var summary = new VoteSummary(
12+
new Dictionary<int, long>
13+
{
14+
[-2] = 0,
15+
[-1] = 3,
16+
[0] = 0,
17+
[1] = 4,
18+
[2] = 8
19+
},
20+
2);
21+
22+
string message = VoteReminderFormatter.Format(summary);
23+
24+
Assert.Contains("<size=50%>(3)</size>", message);
25+
Assert.Contains("<size=50%>(8)</size>", message);
26+
Assert.Contains("[<b><color=#00FF00>++</color></b>]", message);
27+
Assert.DoesNotContain("[<b><color=#80FF00>+</color></b>]", message);
28+
}
29+
30+
[Fact]
31+
public void HighlightsEntireNeutralVoteLabel()
32+
{
33+
var summary = new VoteSummary(new Dictionary<int, long>(), 0);
34+
35+
string message = VoteReminderFormatter.Format(summary);
36+
37+
Assert.Contains("[<b><color=#FFFF00>-+/+-</color></b>]", message);
38+
}
39+
40+
[Fact]
41+
public void MissingKnownGroupsUseZeroCounts()
42+
{
43+
var summary = new VoteSummary(new Dictionary<int, long> { [2] = 8 }, null);
44+
45+
string message = VoteReminderFormatter.Format(summary);
46+
47+
Assert.Equal(4, CountOccurrences(message, "<size=50%>(0)</size>"));
48+
Assert.Equal(1, CountOccurrences(message, "<size=50%>(8)</size>"));
49+
}
50+
51+
[Fact]
52+
public void UnknownSummaryUsesQuestionMarkCountsWithoutHighlight()
53+
{
54+
string message = VoteReminderFormatter.Format(VoteSummary.Unknown);
55+
56+
Assert.Equal(5, CountOccurrences(message, "<size=50%>(?)</size>"));
57+
Assert.DoesNotContain("[<b>", message);
58+
}
59+
60+
[Fact]
61+
public void ReminderPolicyOnlyHidesConfirmedExistingVote()
62+
{
63+
var voted = new VoteSummary(new Dictionary<int, long>(), 1);
64+
var unvoted = new VoteSummary(new Dictionary<int, long>(), null);
65+
66+
Assert.True(VoteReminderFormatter.ShouldShow(true, voted));
67+
Assert.False(VoteReminderFormatter.ShouldShow(false, voted));
68+
Assert.True(VoteReminderFormatter.ShouldShow(false, unvoted));
69+
Assert.True(VoteReminderFormatter.ShouldShow(false, VoteSummary.Unknown));
70+
}
71+
72+
private static int CountOccurrences(string value, string expected)
73+
{
74+
return value.Split(new[] { expected }, StringSplitOptions.None).Length - 1;
75+
}
76+
}

0 commit comments

Comments
 (0)