Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command to initiate a vote for boss-restricted $-commands. #365

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 177 additions & 5 deletions lib/teiserver/coordinator/consul_commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ defmodule Teiserver.Coordinator.ConsulCommands do
"""
@splitter "---------------------------"
@split_delay 60_000
@vote_delay 30_000
@votable_commands ~w(welcome-message rename minratinglevel maxratinglevel setratinglevels resetratinglevels minchevlevel maxchevlevel resetchevlevels)
@spec handle_command(Map.t(), Map.t()) :: Map.t()
@default_ban_reason "Banned"

Expand Down Expand Up @@ -330,6 +332,136 @@ defmodule Teiserver.Coordinator.ConsulCommands do
state
end

def handle_command(
%{command: "cv", remaining: rem, senderid: senderid} = cmd,
%{cmd_voting: nil} = state
) do
[name | args] = String.split(rem, " ")
sender_name = CacheUser.get_username(senderid)

new_state =
cond do
name == "" ->
ChatLib.sayprivateex(
state.coordinator_id,
senderid,
"You must specify a command to be voted on.",
state.lobby_id
)

state

String.starts_with?(name, "$") ->
guess_correct_command =
"$cv " <> String.replace_prefix(name, "$", "") <> " " <> Enum.join(args, " ")

ChatLib.sayprivateex(
state.coordinator_id,
senderid,
"Votes for the '#{name}' command are not allowed. You may try this instead: #{guess_correct_command}",
state.lobby_id
)

state

not Enum.member?(@votable_commands, name) ->
ChatLib.sayprivateex(
state.coordinator_id,
senderid,
"Votes for the '#{name}' command are not allowed",
state.lobby_id
)

state

true ->
ConsulServer.say_command(cmd, state)

[
"#{sender_name} started a vote to run command: $#{rem}",
"Say '$y' to approve, '$n' to reject. You can change your vote at any time.",
"The vote will end in #{round(@vote_delay / 1_000)} seconds. #{sender_name} may say '$ev' to cancel the vote."
]
|> Enum.each(fn msg ->
ChatLib.sayex(
state.coordinator_id,
msg,
state.lobby_id
)
end)

vote_uuid = ExULID.ULID.generate()

new_voting = %{
vote_uuid: vote_uuid,
cmd: rem,
first_voter_id: senderid,
voters: Map.put(%{}, senderid, true)
}

:timer.send_after(@vote_delay, {:do_vote, vote_uuid})
%{state | cmd_voting: new_voting}
end

new_state
end

def handle_command(
%{command: "cv", remaining: _, senderid: senderid} = _,
state
) do
ChatLib.sayprivateex(
state.coordinator_id,
senderid,
"A lobby vote is already in progress, you cannot start a new one yet",
state.lobby_id
)

state
end

def handle_command(
%{command: "ev", remaining: _, senderid: senderid} = _,
%{cmd_voting: nil} = state
) do
ChatLib.sayprivateex(
state.coordinator_id,
senderid,
"There is no lobby vote currently in progress.",
state.lobby_id
)

state
end

def handle_command(
%{command: "ev", remaining: _, senderid: senderid} = _,
%{cmd_voting: %{first_voter_id: first_voter_id}} = state
) do
cond do
senderid != first_voter_id &&
not Enum.member?(state.host_bosses, senderid) &&
not CacheUser.is_moderator?(senderid) ->
ChatLib.sayprivateex(
state.coordinator_id,
senderid,
"Only a boss or the person who started a lobby vote may end it.",
state.lobby_id
)

state

true ->
ChatLib.sayex(
state.coordinator_id,
"The lobby vote was ended.",
state.lobby_id
)

%{state | cmd_voting: nil}
end
end

def handle_command(
%{command: "splitlobby", remaining: rem, senderid: senderid} = cmd,
%{split: nil} = state
Expand Down Expand Up @@ -399,13 +531,13 @@ defmodule Teiserver.Coordinator.ConsulCommands do
state
end

# Split commands for when there is no split happening
def handle_command(%{command: "y"}, %{split: nil} = state), do: state
def handle_command(%{command: "n"}, %{split: nil} = state), do: state
# Split/vote commands for when there is no split/vote happening
def handle_command(%{command: "y"}, %{split: nil, cmd_voting: nil} = state), do: state
def handle_command(%{command: "n"}, %{split: nil, cmd_voting: nil} = state), do: state
def handle_command(%{command: "follow"}, %{split: nil} = state), do: state

# And for when it is
def handle_command(%{command: "n", senderid: senderid} = cmd, state) do
def handle_command(%{command: "n", senderid: senderid} = cmd, %{split: %{}} = state) do
ConsulServer.say_command(cmd, state)
Logger.info("Split.n from #{senderid}")

Expand All @@ -414,7 +546,7 @@ defmodule Teiserver.Coordinator.ConsulCommands do
%{state | split: new_split}
end

def handle_command(%{command: "y", senderid: senderid} = cmd, state) do
def handle_command(%{command: "y", senderid: senderid} = cmd, %{split: %{}} = state) do
ConsulServer.say_command(cmd, state)
Logger.info("Split.y from #{senderid}")

Expand All @@ -423,6 +555,46 @@ defmodule Teiserver.Coordinator.ConsulCommands do
%{state | split: new_split}
end

def handle_command(%{command: "n", senderid: senderid} = cmd, %{cmd_voting: %{}} = state) do
ConsulServer.say_command(cmd, state)
# Logger.info("Vote.n from #{senderid}")

new_voters = Map.put(state.cmd_voting.voters, senderid, false)
new_voting = %{state.cmd_voting | voters: new_voters}
new_state = %{state | cmd_voting: new_voting}

num_ys = ConsulServer.count_votes(new_state, true)
num_ns = ConsulServer.count_votes(new_state, false)

ChatLib.sayex(
state.coordinator_id,
"Current vote totals: $y = #{num_ys}, $n = #{num_ns}",
state.lobby_id
)

new_state
end

def handle_command(%{command: "y", senderid: senderid} = cmd, %{cmd_voting: %{}} = state) do
ConsulServer.say_command(cmd, state)
# Logger.info("Vote.y from #{senderid}")

new_voters = Map.put(state.cmd_voting.voters, senderid, true)
new_voting = %{state.cmd_voting | voters: new_voters}
new_state = %{state | cmd_voting: new_voting}

num_ys = ConsulServer.count_votes(new_state, true)
num_ns = ConsulServer.count_votes(new_state, false)

ChatLib.sayex(
state.coordinator_id,
"Current vote totals: $y = #{num_ys}, $n = #{num_ns}",
state.lobby_id
)

new_state
end

def handle_command(%{command: "follow", remaining: target, senderid: senderid} = cmd, state) do
case ConsulServer.get_user(target, state) do
nil ->
Expand Down
100 changes: 98 additions & 2 deletions lib/teiserver/coordinator/consul_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ defmodule Teiserver.Coordinator.ConsulServer do
alias Teiserver.Data.Types, as: T
alias Teiserver.Coordinator.{ConsulCommands, CoordinatorLib, SpadsParser, CoordinatorCommands}

@always_allow ~w(status s y n follow joinq leaveq splitlobby afks roll players password? newlobby jazlobby tournament)
@boss_commands ~w(balancemode gatekeeper welcome-message meme reset-approval rename minchevlevel maxchevlevel resetchevlevels resetratinglevels minratinglevel maxratinglevel setratinglevels)
@always_allow ~w(status s y n ev follow joinq leaveq splitlobby afks roll players password? newlobby jazlobby tournament)
@boss_commands ~w(balancemode gatekeeper welcome-message meme reset-approval rename minchevlevel maxchevlevel resetchevlevels resetratinglevels minratinglevel maxratinglevel setratinglevels cv)
@player_without_boss_commands ~w(cv)
@vip_boss_commands ~w(shuffle)
@host_commands ~w(specunready makeready settag speclock forceplay lobbyban lobbybanmult unban forcespec forceplay lock unlock makebalance)

Expand Down Expand Up @@ -249,6 +250,12 @@ defmodule Teiserver.Coordinator.ConsulServer do
{:noreply, state}
end

def handle_info({:do_vote, _}, %{cmd_voting: nil} = state) do
# This happens every time someone cancels a lobby vote via "$ev", so logging would be noisy.
# Logger.info("dovote with no vote to do")
{:noreply, state}
end

def handle_info(%{channel: "teiserver_lobby_chat:" <> _, userid: userid, message: msg}, state) do
if state.host_id == userid do
case SpadsParser.handle_in(msg, state) do
Expand Down Expand Up @@ -346,6 +353,51 @@ defmodule Teiserver.Coordinator.ConsulServer do
{:noreply, new_state}
end

def handle_info({:do_vote, vote_uuid}, %{cmd_voting: cmd_voting} = state) do
# Logger.info("Doing vote")

new_state =
if vote_uuid == cmd_voting.vote_uuid do
num_ys = count_votes(state, true)
num_ns = count_votes(state, false)

# Logger.info("Vote tally: #{num_ys} vs #{num_ns}")

if num_ys > num_ns do
ChatLib.sayex(
state.coordinator_id,
"Vote passed. ($y = #{num_ys}, $n = #{num_ns})",
state.lobby_id
)

[name | args] = String.split(cmd_voting.cmd, " ")

cmd = %{
command: name,
remaining: Enum.join(args, " "),
senderid: state.coordinator_id
}

ConsulCommands.handle_command(cmd, state)
else
ChatLib.sayex(
state.coordinator_id,
"Vote did not pass. ($y = #{num_ys}, $n = #{num_ns})",
state.lobby_id
)

state
end
else
# Logger.info("Bad UUID for vote")
state
end

new_state = %{new_state | cmd_voting: nil}

{:noreply, new_state}
end

def handle_info(%{command: command} = cmd, state) do
cond do
CoordinatorCommands.is_coordinator_command?(command) ->
Expand Down Expand Up @@ -493,6 +545,16 @@ defmodule Teiserver.Coordinator.ConsulServer do
{:noreply, new_state}
end

def count_votes(%{cmd_voting: %{voters: votes}} = state, count_yes) do
player_ids =
list_players(state)
|> Enum.map(fn player -> player.userid end)

votes
|> Enum.filter(fn {userid, vote} -> vote == count_yes && Enum.member?(player_ids, userid) end)
|> Enum.count()
end

# Chat handler
@spec handle_lobby_chat(T.userid(), String.t(), map()) :: map()
defp handle_lobby_chat(
Expand Down Expand Up @@ -1011,6 +1073,8 @@ defmodule Teiserver.Coordinator.ConsulServer do
user = Account.get_user_by_id(senderid)

is_host = senderid == state.host_id
is_player = is_player?(state, senderid)
is_boss_present = Enum.count(state.host_bosses) > 0
is_boss = Enum.member?(state.host_bosses, senderid)
is_vip = Enum.member?(user.roles, "VIP")

Expand All @@ -1033,6 +1097,30 @@ defmodule Teiserver.Coordinator.ConsulServer do
Enum.member?(@boss_commands, cmd.command) and (is_host or is_boss) ->
true

Enum.member?(@player_without_boss_commands, cmd.command) and not is_player ->
ChatLib.sayprivateex(
state.coordinator_id,
cmd.senderid,
"You must be either a player or a boss to use '#{cmd.command}'",
state.lobby_id
)

false

Enum.member?(@player_without_boss_commands, cmd.command) and is_boss_present ->
ChatLib.sayprivateex(
state.coordinator_id,
cmd.senderid,
"You can't use '#{cmd.command}' while a boss is present",
state.lobby_id
)

false

Enum.member?(@player_without_boss_commands, cmd.command) and is_player and
not is_boss_present ->
true

Enum.member?(@vip_boss_commands, cmd.command) and (is_vip and is_boss) ->
true

Expand Down Expand Up @@ -1235,6 +1323,13 @@ defmodule Teiserver.Coordinator.ConsulServer do
min(state.host_teamcount * state.host_teamsize, state.player_limit)
end

@spec is_player?(map(), integer()) :: boolean()
def is_player?(state, userid) do
list_players(state)
|> Enum.map(fn %{userid: userid} -> userid end)
|> Enum.member?(userid)
end

@spec list_players(map()) :: [T.client()]
def list_players(%{lobby_id: lobby_id}) do
list_members(%{lobby_id: lobby_id})
Expand Down Expand Up @@ -1383,6 +1478,7 @@ defmodule Teiserver.Coordinator.ConsulServer do
afk_check_list: [],
afk_check_at: nil,
last_seen_map: %{},
cmd_voting: nil,

# Toggle with Coordinator.cast_consul(lobby_id, {:put, :unready_can_play, true})
unready_can_play: false,
Expand Down
3 changes: 3 additions & 0 deletions lib/teiserver/coordinator/coordinator_lib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ defmodule Teiserver.Coordinator.CoordinatorLib do
"Causes a \"vote\" to start where other players can elect to join you in splitting the lobby, follow someone
of their choosing or remain in place. After 60 seconds, if at least the minimum number of players agreed to split, you are moved to a new (empty) lobby and those that voted yes
or are following someone that voted yes are also moved to that lobby.", :everybody},
{"cv", ["command", "[parameters]"],
"Causes a \"vote\" to start on whether to run \"$command [parameters]\", without requiring boss privileges. Votes can only be started for certain commands.",
:everybody},
{"roll", ["range"], "Rolls a random number based on the range format.
- Dice format: nDs, where n is number of dice and s is sides of die. E.g. 4D6 - 4 dice with 6 sides are rolled
- Max format: N, where N is a number and an integer between 1 and N is returned
Expand Down
1 change: 1 addition & 0 deletions lib/teiserver/lobby/schemas/lobby_struct.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ defmodule Teiserver.Lobby.LobbyStruct do
last_queue_state: [],
balance_result: nil,
showmatch: true,
cmd_voting: nil,

# Stuff from the consul that needs to move out of the lobby state itself
split: nil,
Expand Down
Loading