diff --git a/jRandomSkills - SRC Files/src/command/Command.cs b/jRandomSkills - SRC Files/src/command/Command.cs index 7f39b20..c4f9bcf 100644 --- a/jRandomSkills - SRC Files/src/command/Command.cs +++ b/jRandomSkills - SRC Files/src/command/Command.cs @@ -2,6 +2,7 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Commands; +using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Entities.Constants; using CounterStrikeSharp.API.Modules.Utils; using src.menu; @@ -115,14 +116,14 @@ namespace src.command if (command.ArgCount < 2) { - Reply("Usage: css_setoverride "); + Reply("Usage: css_setoverride "); return; } - var targetPlayer = Utilities.GetPlayers().FirstOrDefault(p => p != null && p.IsValid - && (p.SteamID.ToString().Equals(command.GetArg(1), StringComparison.CurrentCultureIgnoreCase) - || p.PlayerName.Equals(command.GetArg(1), StringComparison.OrdinalIgnoreCase))); - if (targetPlayer == null) + // zio: same targets as css_setskill. Several targets are all set to one state: + // on if any of them is off, otherwise off. + var targets = GetSetSkillTargets(player, command.GetArg(1), _ => true); + if (targets.Count == 0) { Reply(player == null ? Localization.GetTranslationWithoutIlliterate("player_not_found_setskill") @@ -130,37 +131,28 @@ namespace src.command return; } - bool enabled = CooldownOverride.Toggle(targetPlayer); - Reply($"Cooldown override {(enabled ? $"{ChatColors.Lime}ON" : $"{ChatColors.LightRed}OFF")}{ChatColors.Default} for {ChatColors.LightRed}\u202A{targetPlayer.PlayerName}\u202C"); + bool enabled = targets.Any(p => !CooldownOverride.IsSet(p)); + foreach (var targetPlayer in targets) + CooldownOverride.Set(targetPlayer, enabled); + + string who = targets.Count == 1 ? targets[0].PlayerName : $"{targets.Count} players"; + Reply($"Cooldown override {(enabled ? $"{ChatColors.Lime}ON" : $"{ChatColors.LightRed}OFF")}{ChatColors.Default} for {ChatColors.LightRed}\u202A{who}\u202C"); } private static void Command_SetSkill(CCSPlayerController? player, CommandInfo command) { Debug.WriteToDebug($"Player {player?.PlayerName} used the css_setskill {command.ArgString} command."); if (!string.IsNullOrEmpty(config.NormalCommands.SetSkillCommand.Permissions) && !AdminManager.PlayerHasPermissions(player, config.NormalCommands.SetSkillCommand.Permissions)) return; - var targetPlayer = Utilities.GetPlayers().FirstOrDefault(p => p != null && p.IsValid - && (p.SteamID.ToString().Equals(command.GetArg(1), StringComparison.CurrentCultureIgnoreCase) - || p.PlayerName.Equals(command.GetArg(1), StringComparison.OrdinalIgnoreCase))); + + void Reply(string key) + { + if (player == null) Server.PrintToConsole(Localization.GetTranslationWithoutIlliterate(key)); + else SkillUtils.PrintToChat(player, player.GetTranslationWithoutIlliterate(key)); + } if (command.ArgCount < 2) { - if (player == null) - { - Server.PrintToConsole(Localization.GetTranslationWithoutIlliterate("correct_form_setskill")); - return; - } - SkillUtils.PrintToChat(player, player.GetTranslationWithoutIlliterate("correct_form_setskill")); - return; - } - - if (targetPlayer == null) - { - if (player == null) - { - Server.PrintToConsole(Localization.GetTranslationWithoutIlliterate("player_not_found_setskill")); - return; - } - SkillUtils.PrintToChat(player, player.GetTranslationWithoutIlliterate("player_not_found_setskill")); + Reply("correct_form_setskill"); return; } @@ -169,45 +161,114 @@ namespace src.command if (skill == null) { - if (player == null) - { - Server.PrintToConsole(Localization.GetTranslationWithoutIlliterate("skill_not_found_setskill")); - return; - } - SkillUtils.PrintToChat(player, player.GetTranslationWithoutIlliterate("skill_not_found_setskill")); + Reply("skill_not_found_setskill"); return; } - var skillPlayer = PlayerManager.GetPlayerByIndex(targetPlayer.Index); - if (skillPlayer != null) + // zio: team-only skills (e.g. ChillOut for CTs) are never given to the other team; those players keep their skill. + int onlyTeam = SkillsInfo.GetValue(skill.Skill, "onlyTeam"); + bool TeamAllowed(CCSPlayerController p) => onlyTeam == 0 || (int)p.Team == onlyTeam; + + var targets = GetSetSkillTargets(player, command.GetArg(1), TeamAllowed); + if (targets.Count == 0) { + Reply("player_not_found_setskill"); + return; + } + + var done = new List(); + var wrongTeam = new List(); + foreach (var targetPlayer in targets) + { + if (!TeamAllowed(targetPlayer)) + { + wrongTeam.Add(targetPlayer); + continue; + } + + var skillPlayer = PlayerManager.GetPlayerByIndex(targetPlayer.Index); + if (skillPlayer == null) continue; + Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [targetPlayer]); skillPlayer.Skill = skill.Skill; skillPlayer.SpecialSkill = Skills.None; Instance.SkillAction(skill.Skill.ToString(), "EnableSkill", [targetPlayer]); Event.UpdateSkillHudExpired(skillPlayer, skill.Skill); + done.Add(targetPlayer); + } - if (player == null) - { - Server.PrintToConsole(Localization.GetTranslationWithoutIlliterate("done_setskill")); - return; - } + string teamName = onlyTeam == (int)CsTeam.Terrorist ? "T" : "CT"; + if (player == null) + { + if (done.Count > 0) + Server.PrintToConsole($"{Localization.GetTranslationWithoutIlliterate("done_setskill")}: {skill.Skill} -> {string.Join(", ", done.Select(p => p.PlayerName))}"); + if (wrongTeam.Count > 0) + Server.PrintToConsole($"{skill.Skill} is {teamName}-only, skipped: {string.Join(", ", wrongTeam.Select(p => p.PlayerName))}"); + if (done.Count == 0 && wrongTeam.Count == 0) + Server.PrintToConsole(Localization.GetTranslationWithoutIlliterate("error_setskill")); + return; + } - SkillUtils.PrintToChat(player, $"{player.GetTranslationWithoutIlliterate("done_setskill")}: {ChatColors.LightRed}{player.GetSkillName(skill.Skill)} {ChatColors.Lime}{player.GetTranslationWithoutIlliterate("for_setskill")} {ChatColors.LightRed}\u202A{targetPlayer.PlayerName}\u202C"); + if (done.Count > 0) + { + string who = done.Count == 1 ? done[0].PlayerName : $"{done.Count} players"; + SkillUtils.PrintToChat(player, $"{player.GetTranslationWithoutIlliterate("done_setskill")}: {ChatColors.LightRed}{player.GetSkillName(skill.Skill)} {ChatColors.Lime}{player.GetTranslationWithoutIlliterate("for_setskill")} {ChatColors.LightRed}\u202A{who}\u202C"); if (skill.Display) SkillUtils.PrintToChat(player, $"{ChatColors.DarkRed}{player.GetSkillName(skill.Skill)}{ChatColors.Lime}: {player.GetSkillDescription(skill.Skill)}", border: "b"); } - else - { - if (player == null) - { - Server.PrintToConsole(Localization.GetTranslationWithoutIlliterate("error_setskill")); - return; - } - SkillUtils.PrintToChat(player, player.GetTranslationWithoutIlliterate("error_setskill")); + if (wrongTeam.Count > 0) + { + string who = wrongTeam.Count == 1 ? wrongTeam[0].PlayerName : $"{wrongTeam.Count} players"; + SkillUtils.PrintToChat(player, $"{ChatColors.LightRed}{player.GetSkillName(skill.Skill)}{ChatColors.Default} is {teamName}-only, not given to {ChatColors.LightRed}\u202A{who}\u202C"); } + + if (done.Count == 0 && wrongTeam.Count == 0) + Reply("error_setskill"); + } + + // zio: one player for commands that take a single target (css_setstaticskill, css_next): the + // css_setskill rules, so part of a name works, but only when it picks out one player. + private static CCSPlayerController? GetSinglePlayerTarget(CCSPlayerController? caller, string arg) + { + if (string.IsNullOrEmpty(arg)) return null; + var targets = GetSetSkillTargets(caller, arg, _ => true); + return targets.Count == 1 ? targets[0] : null; + } + + // zio: css_setskill targets. An exact name or SteamID64 as before, CSS's own selectors + // (@me, @all, @ct, @t, @alive, #userid, ...), or @random/@randomct/@randomt: one random + // alive player, drawn only from players the skill's team restriction allows. + private static List GetSetSkillTargets(CCSPlayerController? caller, string arg, Func teamAllowed) + { + var players = Utilities.GetPlayers().Where(p => p != null && p.IsValid && !p.IsHLTV).ToList(); + + CsTeam? randomTeam = arg.ToLowerInvariant() switch + { + "@random" => CsTeam.None, + "@randomct" => CsTeam.CounterTerrorist, + "@randomt" => CsTeam.Terrorist, + _ => null + }; + if (randomTeam != null) + { + var pool = players.Where(p => (p.Team == CsTeam.Terrorist || p.Team == CsTeam.CounterTerrorist) + && (randomTeam == CsTeam.None || p.Team == randomTeam) + && p.PawnIsAlive && teamAllowed(p)).ToList(); + return pool.Count == 0 ? [] : [pool[Random.Shared.Next(pool.Count)]]; + } + + var exact = players.FirstOrDefault(p => p.SteamID.ToString().Equals(arg, StringComparison.CurrentCultureIgnoreCase) + || p.PlayerName.Equals(arg, StringComparison.OrdinalIgnoreCase)); + if (exact != null) return [exact]; + + var matched = new Target(arg).GetTarget(caller).Players.Where(p => p != null && p.IsValid && !p.IsHLTV).ToList(); + if (arg.StartsWith('@')) + return [.. matched.Where(p => p.Team == CsTeam.Terrorist || p.Team == CsTeam.CounterTerrorist)]; + + // A partial name only counts when it matches one player, so a typo can't hit several. + return matched.Count == 1 ? matched : []; } [CommandHelper(minArgs: 0, whoCanExecute: CommandUsage.CLIENT_ONLY)] @@ -522,9 +583,7 @@ namespace src.command { Debug.WriteToDebug($"Player {player?.PlayerName} used the css_setstaticskill {command.ArgString} command."); if (!string.IsNullOrEmpty(config.NormalCommands.SetStaticSkillCommand.Permissions) && !AdminManager.PlayerHasPermissions(player, config.NormalCommands.SetStaticSkillCommand.Permissions)) return; - var targetPlayer = Utilities.GetPlayers().FirstOrDefault(p => p != null && p.IsValid - && (p.SteamID.ToString().Equals(command.GetArg(1), StringComparison.CurrentCultureIgnoreCase) - || p.PlayerName.Equals(command.GetArg(1), StringComparison.OrdinalIgnoreCase))); + var targetPlayer = GetSinglePlayerTarget(player, command.GetArg(1)); if (command.ArgCount < 2) { @@ -671,10 +730,8 @@ namespace src.command if (!string.IsNullOrEmpty(playerString)) { - targetPlayer = Utilities.GetPlayers().FirstOrDefault(p => p != null && p.IsValid - && (p.SteamID.ToString().Equals(playerString, StringComparison.CurrentCultureIgnoreCase) - || p.PlayerName.Equals(playerString, StringComparison.OrdinalIgnoreCase))); - + targetPlayer = GetSinglePlayerTarget(player, playerString); + if (targetPlayer == null) { if (player == null) diff --git a/jRandomSkills - SRC Files/src/jRandomSkills.cs b/jRandomSkills - SRC Files/src/jRandomSkills.cs index f80a684..0053b3e 100644 --- a/jRandomSkills - SRC Files/src/jRandomSkills.cs +++ b/jRandomSkills - SRC Files/src/jRandomSkills.cs @@ -44,7 +44,7 @@ namespace src public override string ModuleName => "[CS2] [ jRandomSkills ]"; public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)"; public override string ModuleDescription => "Plugin adds random skills every round for CS2 by D3X. Modified by Juzlus."; - public override string ModuleVersion => "1.2.4.b3-panorama2"; + public override string ModuleVersion => "1.2.4.b3-panorama5"; public override void Load(bool hotReload) { diff --git a/jRandomSkills - SRC Files/src/lang/en.json b/jRandomSkills - SRC Files/src/lang/en.json index 7ab81f0..fc5b26f 100644 --- a/jRandomSkills - SRC Files/src/lang/en.json +++ b/jRandomSkills - SRC Files/src/lang/en.json @@ -633,7 +633,7 @@ "skill_not_found_setskill": "No such CHATCOLORS.REDskill found", "player_not_found_setskill": "No such CHATCOLORS.REDplayer found", - "correct_form_setskill": "Correct usage: CHATCOLORS.RED!setskill ", + "correct_form_setskill": "Correct usage: CHATCOLORS.RED!setskill ", "correct_form_setscore": "Correct usage: CHATCOLORS.RED!setscore ", "done_setskill": "skill set", "error_setskill": "Failed to set CHATCOLORS.REDskill", diff --git a/jRandomSkills - SRC Files/src/utils/CooldownOverride.cs b/jRandomSkills - SRC Files/src/utils/CooldownOverride.cs index 0a8992e..d47654e 100644 --- a/jRandomSkills - SRC Files/src/utils/CooldownOverride.cs +++ b/jRandomSkills - SRC Files/src/utils/CooldownOverride.cs @@ -15,14 +15,16 @@ namespace src.utils private static readonly HashSet _players = []; private static readonly ConcurrentDictionary _stateFields = new(); - // Toggles the override and returns the new state. - public static bool Toggle(CCSPlayerController player) + // Sets the override for one player. + public static void Set(CCSPlayerController player, bool enabled) { - if (_players.Remove(Key(player))) return false; - _players.Add(Key(player)); - return true; + if (enabled) _players.Add(Key(player)); + else _players.Remove(Key(player)); } + // The player's own setting, without following a bot takeover like Has does. + public static bool IsSet(CCSPlayerController player) => _players.Contains(Key(player)); + // A bot a human has taken over counts as that human, since skills run on the bot's controller. public static bool Has(CCSPlayerController? player) { diff --git a/release.sh b/release.sh index c02fb80..9b76855 100755 --- a/release.sh +++ b/release.sh @@ -29,8 +29,13 @@ set -euo pipefail FORGEJO_URL="https://git.zio.sh" OWNER="cs2" REPO="jRandomSkills" -# Placeholder - replace, or set FORGEJO_TOKEN in the environment instead. Needs write:repository. -# Only uploading needs it; downloading from the public repo doesn't. Don't commit a real token. +# Needs write:repository. Only uploading needs it; downloading from the public repo doesn't. From the +# environment, or else from .forgejo-token in the cs2/ folder above every plugin (outside their +# repos, mode 600). Don't commit a real token. +TOKEN_FILE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/../../.forgejo-token" +if [[ -z "${FORGEJO_TOKEN:-}" && -r "$TOKEN_FILE" ]]; then + FORGEJO_TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +fi FORGEJO_TOKEN="${FORGEJO_TOKEN:-CHANGE_ME_FORGEJO_TOKEN}" # CS2 client used to compile the Panorama layouts, and the Workshop Tools addon compiled into. @@ -60,7 +65,7 @@ if [[ -z "$TAG" ]]; then exit 1 fi if [[ "$FORGEJO_TOKEN" == "CHANGE_ME_FORGEJO_TOKEN" ]]; then - echo "Set FORGEJO_TOKEN (edit release.sh or export it) first." >&2 + echo "Set FORGEJO_TOKEN (export it, or put it in $TOKEN_FILE) first." >&2 exit 1 fi