diff --git a/Content.Client/Voting/UI/VotePopup.xaml.cs b/Content.Client/Voting/UI/VotePopup.xaml.cs index b9ff4dd7bd0..b19f906e7b7 100644 --- a/Content.Client/Voting/UI/VotePopup.xaml.cs +++ b/Content.Client/Voting/UI/VotePopup.xaml.cs @@ -38,7 +38,7 @@ public VotePopup(VoteManager.ActiveVote vote) Modulate = Color.White.WithAlpha(0.75f); _voteButtons = new Button[vote.Entries.Length]; - var group = new ButtonGroup(); + var group = _vote.Multivariate ? null : new ButtonGroup(); // Erida-edit for (var i = 0; i < _voteButtons.Length; i++) { @@ -70,9 +70,17 @@ public void UpdateData() { _voteButtons[i].Text = Loc.GetString("ui-vote-button-no-votes", ("text", entry.Text)); } - - if (_vote.OurVote == i) - _voteButtons[i].Pressed = true; + // Erida-start + if (_vote.OurVotes != null) + if (_vote.OurVotes.Contains(i)) + { + _voteButtons[i].Pressed = true; + } + else + { + _voteButtons[i].Pressed = false; + } + // Erida-end } } diff --git a/Content.Client/Voting/VoteManager.cs b/Content.Client/Voting/VoteManager.cs index 629adb36aa5..efa99f7470e 100644 --- a/Content.Client/Voting/VoteManager.cs +++ b/Content.Client/Voting/VoteManager.cs @@ -155,9 +155,9 @@ private void ReceiveVoteData(MsgVoteData message) { Entries = message.Options .Select(c => new VoteEntry(c.name)) - .ToArray() + .ToArray(), + Multivariate = message.Multivariate // Erida-edit }; - existingVote = vote; _votes.Add(voteId, vote); } @@ -176,8 +176,9 @@ private void ReceiveVoteData(MsgVoteData message) } // Update vote data from incoming. - if (message.IsYourVoteDirty) - existingVote.OurVote = message.YourVote; + if (message.IsYourVoteDirty + && message.YourVotes != null) // Erida-edit + existingVote.OurVotes = message.YourVotes.Select(b => (int)b).ToList(); // Erida-edit // On the server, most of these params can't change. // It can't hurt to just re-set this stuff since I'm lazy and the server is sending it anyways, so... existingVote.Initiator = message.VoteInitiator; @@ -186,6 +187,7 @@ private void ReceiveVoteData(MsgVoteData message) existingVote.EndTime = _gameTiming.RealServerToLocal(message.EndTime); existingVote.DisplayVotes = message.DisplayVotes; existingVote.TargetEntity = message.TargetEntity; + existingVote.Multivariate = message.Multivariate; // Erida-edit // Logger.Debug($"{existingVote.StartTime}, {existingVote.EndTime}, {_gameTiming.RealTime}"); @@ -232,7 +234,17 @@ public void SendCastVote(int voteId, int option) var data = _votes[voteId]; // Update immediately to avoid any funny reconciliation bugs. // See also code in server side to avoid bulldozing this. - data.OurVote = option; + // Erida-start + if (data.Multivariate + && data.OurVotes != null) + { + data.OurVotes.Add(option); + } + else + { + data.OurVotes = new List { option }; + } + // Erida-end _console.LocalShell.RemoteExecuteCommand($"vote {voteId} {option}"); } @@ -245,10 +257,11 @@ public sealed class ActiveVote public TimeSpan EndTime; public string Title = ""; public string Initiator = ""; - public int? OurVote; + public List? OurVotes = new(); // Erida-edit public int Id; public bool DisplayVotes; public int? TargetEntity; // NetEntity + public bool Multivariate; // Erida-edit public ActiveVote(int voteId) { Id = voteId; diff --git a/Content.Server/Voting/IVoteHandle.cs b/Content.Server/Voting/IVoteHandle.cs index f5c31c5b1eb..0165353127d 100644 --- a/Content.Server/Voting/IVoteHandle.cs +++ b/Content.Server/Voting/IVoteHandle.cs @@ -46,7 +46,7 @@ public interface IVoteHandle /// /// Dictionary of votes cast by players, matching the option's id. /// - IReadOnlyDictionary CastVotes { get; } + IReadOnlyDictionary> CastVotes { get; } // Erida-edit /// /// Current count of votes per option type. diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index e2ee3c38057..0859409963f 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -226,7 +226,8 @@ private void CreatePresetVote(ICommonSession? initiator) Title = Loc.GetString("ui-vote-gamemode-title"), Duration = alone ? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone)) - : TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerPreset)) + : TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerPreset)), + Multivariate = true // Erida-edit }; if (alone) @@ -272,7 +273,8 @@ private void CreateMapVote(ICommonSession? initiator) Title = Loc.GetString("ui-vote-map-title"), Duration = alone ? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone)) - : TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerMap)) + : TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerMap)), + Multivariate = true // Erida-edit }; if (alone) @@ -488,11 +490,11 @@ private async void CreateVotekickVote(ICommonSession? initiator, string[]? args) List noVoters = new(); foreach (var (voter, castVote) in vote.CastVotes) { - if (castVote == 0) + if (castVote[0] == 0) // Erida-edit { yesVoters.Add(voter); } - if (castVote == 1) + if (castVote[1] == 1) // Erida-edit { noVoters.Add(voter); } diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index 94e637638ee..766e0bce7ae 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -113,22 +113,53 @@ private void CastVote(VoteReg v, ICommonSession player, int? option) { if (!IsValidOption(v, option)) throw new ArgumentOutOfRangeException(nameof(option), "Invalid vote option ID"); - - if (v.CastVotes.TryGetValue(player, out var existingOption)) + // Erida-start + if (v.Multivariate) { - v.Entries[existingOption].Votes -= 1; - } + if (!v.CastVotes.ContainsKey(player)) + { + v.CastVotes[player] = new List(); + } - if (option != null) - { - v.Entries[option.Value].Votes += 1; - v.CastVotes[player] = option.Value; + var playerVotes = v.CastVotes[player]; + + if (option != null) + { + if (playerVotes.Contains(option.Value)) + { + playerVotes.Remove(option.Value); + v.Entries[option.Value].Votes -= 1; + } + else + { + v.Entries[option.Value].Votes += 1; + playerVotes.Add(option.Value); + } + + if (playerVotes.Count == 0) + { + v.CastVotes.Remove(player); // ??? + } + } } else { - v.CastVotes.Remove(player); - } + if (v.CastVotes.TryGetValue(player, out var existingOption)) + { + v.Entries[existingOption[0]].Votes -= 1; + } + if (option != null) + { + v.Entries[option.Value].Votes += 1; + v.CastVotes[player] = new List { option.Value }; + } + else + { + v.CastVotes.Remove(player); + } + } + // Erida-end v.VotesDirty.Add(player); v.Dirty = true; } @@ -211,7 +242,7 @@ public IVoteHandle CreateVote(VoteOptions options) var start = _timing.RealTime; var end = start + options.Duration; var reg = new VoteReg(id, entries, options.Title, options.InitiatorText, - options.InitiatorPlayer, start, end, options.VoterEligibility, options.DisplayVotes, options.TargetEntity); + options.InitiatorPlayer, start, end, options.VoterEligibility, options.DisplayVotes, options.TargetEntity, options.Multivariate); // Erida-edit var handle = new VoteHandle(this, reg); @@ -260,6 +291,7 @@ private void SendSingleUpdate(VoteReg v, ICommonSession player) msg.VoteInitiator = v.InitiatorText; msg.StartTime = v.StartTime; msg.EndTime = v.EndTime; + msg.Multivariate = v.Multivariate; // Erida-edit if (v.TargetEntity != null) { @@ -276,7 +308,7 @@ private void SendSingleUpdate(VoteReg v, ICommonSession player) msg.IsYourVoteDirty = dirty; if (dirty) { - msg.YourVote = (byte) cast; + msg.YourVotes = cast.Select(c => (byte)c).ToArray(); // Erida-edit } } @@ -387,7 +419,12 @@ private void EndVote(VoteReg v) { if (!CheckVoterEligibility(playerVote.Key, v.VoterEligibility)) { - v.Entries[playerVote.Value].Votes -= 1; + // Erida-start + foreach (var value in playerVote.Value) + { + v.Entries[value].Votes -= 1; + } + // Erida-end v.CastVotes.Remove(playerVote.Key); } } @@ -496,7 +533,7 @@ private void WirePresetVoteInitiator(VoteOptions options, ICommonSession? player private sealed class VoteReg { public readonly int Id; - public readonly Dictionary CastVotes = new(); + public readonly Dictionary> CastVotes = new(); // Erida-edit public readonly VoteEntry[] Entries; public readonly string Title; public readonly string InitiatorText; @@ -506,6 +543,7 @@ private sealed class VoteReg public readonly VoterEligibility VoterEligibility; public readonly bool DisplayVotes; public readonly NetEntity? TargetEntity; + public readonly bool Multivariate; // Erida-edit public bool Cancelled; public bool Finished; @@ -516,7 +554,7 @@ private sealed class VoteReg public ICommonSession? Initiator { get; } public VoteReg(int id, VoteEntry[] entries, string title, string initiatorText, - ICommonSession? initiator, TimeSpan start, TimeSpan end, VoterEligibility voterEligibility, bool displayVotes, NetEntity? targetEntity) + ICommonSession? initiator, TimeSpan start, TimeSpan end, VoterEligibility voterEligibility, bool displayVotes, NetEntity? targetEntity, bool multivariate) // Erida-edit { Id = id; Entries = entries; @@ -528,6 +566,7 @@ public VoteReg(int id, VoteEntry[] entries, string title, string initiatorText, VoterEligibility = voterEligibility; DisplayVotes = displayVotes; TargetEntity = targetEntity; + Multivariate = multivariate; // Erida-edit } } @@ -567,7 +606,7 @@ private sealed class VoteHandle : IVoteHandle public string InitiatorText => _reg.InitiatorText; public bool Finished => _reg.Finished; public bool Cancelled => _reg.Cancelled; - public IReadOnlyDictionary CastVotes => _reg.CastVotes; + public IReadOnlyDictionary> CastVotes => _reg.CastVotes; // Erida-edit public IReadOnlyDictionary VotesPerOption { get; } diff --git a/Content.Server/Voting/VoteOptions.cs b/Content.Server/Voting/VoteOptions.cs index e0647fb2d0f..3fb66878c60 100644 --- a/Content.Server/Voting/VoteOptions.cs +++ b/Content.Server/Voting/VoteOptions.cs @@ -50,7 +50,7 @@ public sealed class VoteOptions public bool DisplayVotes = true; /// - /// Whether the vote should have an entity attached to it, to be used for things like letting ghosts follow it. + /// Whether the vote should have an entity attached to it, to be used for things like letting ghosts follow it. /// public NetEntity? TargetEntity = null; @@ -75,5 +75,7 @@ public void SetInitiatorOrServer(ICommonSession? player) InitiatorText = Loc.GetString("vote-options-server-initiator-text"); } } + + public bool Multivariate = false; // Erida-edit } } diff --git a/Content.Shared/Voting/MsgVoteData.cs b/Content.Shared/Voting/MsgVoteData.cs index c9d9d49887e..9d264a5f2bf 100644 --- a/Content.Shared/Voting/MsgVoteData.cs +++ b/Content.Shared/Voting/MsgVoteData.cs @@ -16,9 +16,10 @@ public sealed class MsgVoteData : NetMessage public TimeSpan EndTime; // Server RealTime. public (ushort votes, string name)[] Options = default!; public bool IsYourVoteDirty; - public byte? YourVote; + public byte[]? YourVotes; // Erida-edit public bool DisplayVotes; public int TargetEntity; + public bool Multivariate; // Erida-edit public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { @@ -35,6 +36,7 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer EndTime = TimeSpan.FromTicks(buffer.ReadInt64()); DisplayVotes = buffer.ReadBoolean(); TargetEntity = buffer.ReadVariableInt32(); + Multivariate = buffer.ReadBoolean(); // Erida-edit Options = new (ushort votes, string name)[buffer.ReadByte()]; for (var i = 0; i < Options.Length; i++) @@ -45,8 +47,22 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer IsYourVoteDirty = buffer.ReadBoolean(); if (IsYourVoteDirty) { - YourVote = buffer.ReadBoolean() ? buffer.ReadByte() : null; + // Erida-start + if (buffer.ReadBoolean()) + { + var voteCount = buffer.ReadByte(); + YourVotes = new byte[voteCount]; + for (var i = 0; i < voteCount; i++) + { + YourVotes[i] = buffer.ReadByte(); + } + } + else + { + YourVotes = null; + } } + // Erida-end } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) @@ -64,6 +80,7 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer buffer.Write(EndTime.Ticks); buffer.Write(DisplayVotes); buffer.WriteVariableInt32(TargetEntity); + buffer.Write(Multivariate); // Erida-edit buffer.Write((byte) Options.Length); foreach (var (votes, name) in Options) @@ -75,12 +92,18 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer buffer.Write(IsYourVoteDirty); if (IsYourVoteDirty) { - buffer.Write(YourVote.HasValue); - if (YourVote.HasValue) + // Erida-start + buffer.Write(YourVotes != null); + if (YourVotes != null) { - buffer.Write(YourVote.Value); + buffer.Write((byte) YourVotes.Length); + foreach (var vote in YourVotes) + { + buffer.Write(vote); + } } } + // Erida-end } public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableOrdered; diff --git a/Resources/Prototypes/_Erida/game_presets.yml b/Resources/Prototypes/_Erida/game_presets.yml index e1af16c585e..987f151022e 100644 --- a/Resources/Prototypes/_Erida/game_presets.yml +++ b/Resources/Prototypes/_Erida/game_presets.yml @@ -12,7 +12,7 @@ - type: gamePreset id: LightDynamic name: light-dynamic-title - showInVote: false + showInVote: true description: light-dynamic-description rules: - MeteorSwarmScheduler @@ -23,7 +23,7 @@ - type: gamePreset id: HardDynamic name: hard-dynamic-title - showInVote: false + showInVote: true description: hard-dynamic-description rules: - RampingStationEventScheduler diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index 39935fd35ec..4d21c65be65 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -3,7 +3,7 @@ alias: - survival name: survival-title - showInVote: false # secret + showInVote: true # secret # Erida-edit description: survival-description rules: - MeteorSwarmScheduler @@ -19,7 +19,7 @@ - junk - meteorhell name: kessler-syndrome-title - showInVote: false # secret + showInVote: true # secret # Erida-edit description: kessler-syndrome-description rules: - KesslerSyndromeScheduler @@ -55,7 +55,7 @@ - punishment name: aller-at-once-title description: aller-at-once-description - showInVote: false #Please god dont do this + showInVote: true #Please god dont do this # Erida-edit rules: - Nukeops - Traitor @@ -143,7 +143,7 @@ - tator name: traitor-title description: traitor-description - showInVote: false + showInVote: true # Erida-edit rules: - DummyNonAntagChance - Traitor @@ -172,7 +172,7 @@ - nukeops name: nukeops-title description: nukeops-description - showInVote: false + showInVote: true # Erida-edit rules: - Nukeops - DummyNonAntagChance @@ -190,7 +190,7 @@ - revolutionaries name: rev-title description: rev-description - showInVote: false + showInVote: true # Erida-edit rules: - DummyNonAntagChance - Revolutionary @@ -206,7 +206,7 @@ - wizard name: wizard-title description: wizard-description - showInVote: false + showInVote: true # Erida-edit rules: - Wizard - DummyNonAntagChance @@ -222,7 +222,7 @@ - xenoborgs name: xenoborgs-title description: xenoborgs-description - showInVote: false + showInVote: true # Erida-edit rules: - Xenoborgs - DummyNonAntagChance @@ -242,7 +242,7 @@ - zomber name: zombie-title description: zombie-description - showInVote: false + showInVote: true # Erida-edit rules: - Zombie - BasicStationEventScheduler @@ -258,7 +258,7 @@ - meteombies name: zombieteors-title description: zombieteors-description - showInVote: false + showInVote: true # Erida-edit rules: - Zombie - BasicStationEventScheduler