feat: Panorama skill HUD and menu, CSS 1.0.375
- Skill card and W/S/E target menu drawn as CCSCustomHudLayout panels via PanoramaManager (panorama/ sources), replacing PrintToCenterHtml and WASDMenuAPI; WASDMenuAPI.dll dropped - Custom status lines (cooldowns, charges) overwrite the description on the card; Medic keeps its description with the charge count below - css_setoverride toggles a per-player cooldown bypass - YourSkillChatInfo also gates the round-start skill description in chat - Language: "[css_useSkill]" becomes the configured button name, "Press ... key" wording, welcome message trimmed to its first line - Build against CounterStrikeSharp.API 1.0.375
This commit is contained in:
parent
20dafd2a7c
commit
793370a55f
37 changed files with 1076 additions and 415 deletions
|
|
@ -863,7 +863,16 @@ namespace src.player
|
|||
|
||||
private static readonly Dictionary<uint, jSkill_SkillInfo> nextRoundPicks = [];
|
||||
|
||||
public static void UpdateSkillHUD(CCSPlayerController? player, jSkill_PlayerInfo? skillPlayer, string? headerLine, string? centerLine, string? extraLine, bool isDescription)
|
||||
// Matches any <tag> or </tag> - used only to strip the HTML jRandomSkills already builds
|
||||
// into centerLine/extraLine (<font color=...>) before handing plain text to Panorama, which
|
||||
// can't render injected HTML and can only take a color via a CSS class instead.
|
||||
private static readonly Dictionary<int, PanoramaHudCache> panoramaHudCaches = [];
|
||||
|
||||
private static readonly Regex HtmlTagPattern = new("<[^>]+>", RegexOptions.Compiled);
|
||||
|
||||
public static void UpdateSkillHUD(
|
||||
CCSPlayerController? player, jSkill_PlayerInfo? skillPlayer, string? headerLine, string? centerLine,
|
||||
string? extraLine, bool isDescription, Skills skillForColor = Skills.None)
|
||||
{
|
||||
lock (setLock)
|
||||
{
|
||||
|
|
@ -875,6 +884,15 @@ namespace src.player
|
|||
extraLine = Illiterate.GetRandomText(extraLine);
|
||||
}
|
||||
|
||||
// Every skill draws through the Panorama card now. A skill's PrintHTML (cooldowns,
|
||||
// fuel, countdowns - isDescription false) replaces the description line; the
|
||||
// PrintToCenterHtml path below is only a fallback if the panel failed to spawn.
|
||||
if (Instance.SkillHud != null)
|
||||
{
|
||||
UpdatePanoramaSkillHud(player, skillPlayer, centerLine, extraLine, isDescription, skillForColor);
|
||||
return;
|
||||
}
|
||||
|
||||
var config = Config.LoadedConfig.HtmlHudCustomisation;
|
||||
|
||||
var cache = skillPlayer?.HudCache;
|
||||
|
|
@ -922,5 +940,99 @@ namespace src.player
|
|||
player.PrintToCenterHtml(hudContent);
|
||||
}
|
||||
}
|
||||
|
||||
// The Panorama half of UpdateSkillHUD above. Change-detected per field via
|
||||
// panoramaHudCaches, the same idea as HudCacheEntry but per-field rather than one content blob, since
|
||||
// SetVariableFor/SetClassFor are separate native calls rather than one PrintToCenterHtml.
|
||||
private static void UpdatePanoramaSkillHud(
|
||||
CCSPlayerController player, jSkill_PlayerInfo? skillPlayer, string? centerLine,
|
||||
string? extraLine, bool isDescription, Skills skillForColor)
|
||||
{
|
||||
var skillHud = Instance.SkillHud;
|
||||
if (skillHud == null || skillPlayer == null) return;
|
||||
|
||||
// centerLine/extraLine arrive pre-wrapped in <font color=...> for the PrintToCenterHtml
|
||||
// path above - Panorama can't render injected HTML, and color comes from the
|
||||
// rarity-* class below instead, so the markup is just stripped rather than parsed.
|
||||
string skillText = ToPlainText(centerLine);
|
||||
string extraText = ToPlainText(extraLine);
|
||||
string rarityClass = "rarity-" + SkillsInfo.GetValue<string>(skillForColor, "Rarity").ToLowerInvariant();
|
||||
|
||||
// Keyed by the viewer's slot, not skillPlayer: while controlling a bot (or spectating)
|
||||
// skillPlayer is someone else's record, but the panel being drawn is this player's.
|
||||
if (!panoramaHudCaches.TryGetValue(player.Slot, out var cache))
|
||||
panoramaHudCaches[player.Slot] = cache = new PanoramaHudCache();
|
||||
cache.TouchedThisTick = true;
|
||||
|
||||
// Open() before any SetVariableFor/SetClassFor - both silently no-op without a session,
|
||||
// and Open() is what creates it (same lesson as PanoramaHudDemo's own OpenSkillMenu).
|
||||
if (!cache.Open)
|
||||
{
|
||||
skillHud.Open(player);
|
||||
cache.Open = true;
|
||||
}
|
||||
|
||||
if (cache.Skill != skillText || cache.RarityClass != rarityClass)
|
||||
{
|
||||
skillHud.SetVariableFor(player, "hud_skill", skillText);
|
||||
// Take the old rarity class off before adding the new one - Panorama has no
|
||||
// mutually-exclusive class group, so both would otherwise stay on and whichever the
|
||||
// stylesheet happens to list last would win, not whichever was set last.
|
||||
if (cache.RarityClass != null)
|
||||
skillHud.SetClassFor(player, "skillhud_skill", cache.RarityClass, false);
|
||||
skillHud.SetClassFor(player, "skillhud_skill", rarityClass, true);
|
||||
cache.Skill = skillText;
|
||||
cache.RarityClass = rarityClass;
|
||||
}
|
||||
|
||||
if (cache.Extra != extraText)
|
||||
{
|
||||
skillHud.SetVariableFor(player, "hud_extra", extraText);
|
||||
skillHud.SetClassFor(player, "skillhud_extra", "empty", string.IsNullOrWhiteSpace(extraText));
|
||||
cache.Extra = extraText;
|
||||
}
|
||||
|
||||
if (cache.IsInfo != !isDescription)
|
||||
{
|
||||
skillHud.SetClassFor(player, "skillhud_extra", "info", !isDescription);
|
||||
cache.IsInfo = !isDescription;
|
||||
}
|
||||
}
|
||||
|
||||
// Closes the Panorama skill HUD for anyone whose cache says it's open but who wasn't
|
||||
// touched this HUD frame - warmup started, a menu opened, HUD got suppressed, or any of
|
||||
// PlayerOnTick.cs's other early-return conditions before it ever reaches UpdateSkillHUD.
|
||||
// Without this the panel would just keep showing stale content indefinitely, unlike
|
||||
// PrintToCenterHtml which expires on its own after a few seconds. Called once per HUD frame
|
||||
// from PlayerOnTick.cs, after the per-player loop, for exactly this reason.
|
||||
private static string ToPlainText(string? html)
|
||||
=> System.Net.WebUtility.HtmlDecode(
|
||||
HtmlTagPattern.Replace((html ?? "").Replace("<br>", "\n", StringComparison.OrdinalIgnoreCase), ""));
|
||||
|
||||
public static void ClosePanoramaSkillHudForUntouched()
|
||||
{
|
||||
var skillHud = Instance.SkillHud;
|
||||
if (skillHud == null) return;
|
||||
|
||||
foreach (var (slot, cache) in panoramaHudCaches)
|
||||
{
|
||||
if (!cache.Open) continue;
|
||||
|
||||
if (cache.TouchedThisTick)
|
||||
{
|
||||
cache.TouchedThisTick = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var player = Utilities.GetPlayerFromSlot(slot);
|
||||
if (player is { IsValid: true }) skillHud.Close(player);
|
||||
|
||||
cache.Open = false;
|
||||
cache.Skill = null;
|
||||
cache.Extra = null;
|
||||
cache.RarityClass = null;
|
||||
cache.IsInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
|
|
@ -33,6 +33,9 @@ namespace src.player
|
|||
if (player != null && player.IsValid)
|
||||
UpdatePlayerHud(player, now);
|
||||
}
|
||||
// Closes the Panorama skill HUD for anyone whose panel is open but who wasn't
|
||||
// touched this frame - see its own doc comment for why that's needed at all.
|
||||
Event.ClosePanoramaSkillHudForUntouched();
|
||||
PerfLog.Sample("OnTick(hud)", perfStart);
|
||||
});
|
||||
|
||||
|
|
@ -62,7 +65,10 @@ namespace src.player
|
|||
var gameRulesProxy = Utilities.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules").FirstOrDefault();
|
||||
|
||||
if (gameRulesProxy != null)
|
||||
{
|
||||
Instance.GameRules = gameRulesProxy.GameRules;
|
||||
Instance.GameRulesProxy = gameRulesProxy;
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateGameRules()
|
||||
|
|
@ -70,7 +76,17 @@ namespace src.player
|
|||
if (Instance?.GameRules == null || Instance.GameRules.Handle == IntPtr.Zero)
|
||||
InitializeGameRules();
|
||||
else if (Instance != null && Config.LoadedConfig.EnableFlashingHtmlHudFix && !Instance.GameRules.WarmupPeriod)
|
||||
Instance.GameRules.GameRestart = Instance.GameRules.RestartRoundTime < Server.CurrentTime;
|
||||
{
|
||||
bool restart = Instance.GameRules.RestartRoundTime < Server.CurrentTime;
|
||||
if (Instance.GameRules.GameRestart == restart) return;
|
||||
Instance.GameRules.GameRestart = restart;
|
||||
|
||||
// The schema write has no networking hook of its own. GameRules isn't embedded in the
|
||||
// proxy entity, so a CCSGameRules field offset can't be resolved against it ("N not
|
||||
// resolved" spam) - mark the proxy's m_pGameRules pointer changed instead.
|
||||
if (Instance.GameRulesProxy != null && Instance.GameRulesProxy.Handle != IntPtr.Zero)
|
||||
Utilities.SetStateChanged(Instance.GameRulesProxy, "CCSGameRulesProxy", "m_pGameRules");
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdatePlayerHud(CCSPlayerController player, DateTime now)
|
||||
|
|
@ -97,6 +113,10 @@ namespace src.player
|
|||
string infoLine = string.Empty;
|
||||
string skillLine = string.Empty;
|
||||
string remainingLine = string.Empty;
|
||||
// Which skill's Rarity should color the Panorama HUD (see UpdatePanoramaSkillHud) -
|
||||
// not always skillPlayer.Skill, since the observer branch below shows a DIFFERENT
|
||||
// player's skill, and the drawing-flicker branch shows a random one each frame.
|
||||
Skills skillForColor = Skills.None;
|
||||
|
||||
bool showDescriptionHUD = skillPlayer.SkillDescriptionHudExpired >= now || Config.LoadedConfig.DisplayAlwaysDescription;
|
||||
bool isDescription = true;
|
||||
|
|
@ -114,6 +134,7 @@ namespace src.player
|
|||
|
||||
infoLine = player.GetTranslationWithoutIlliterate("drawing_skill");
|
||||
skillLine = $"<font color='{randomSkill.Color}'>{player.GetSkillName(randomSkill.Skill)}</font>";
|
||||
skillForColor = randomSkill.Skill;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -123,8 +144,8 @@ namespace src.player
|
|||
|
||||
if (skillInfo != null)
|
||||
{
|
||||
infoLine = player.GetTranslationWithoutIlliterate("your_skill");
|
||||
skillLine = $"<font color='{skillInfo.Color}'>{player.GetSkillName(skillInfo.Skill, skillPlayer.SkillChance)}</font>";
|
||||
skillForColor = skillInfo.Skill;
|
||||
|
||||
if (skillInfo.Skill != Skills.None)
|
||||
{
|
||||
|
|
@ -175,11 +196,17 @@ namespace src.player
|
|||
infoLine = string.IsNullOrEmpty(observerSkill) ? pName : $"{observerSkill} {pName}";
|
||||
|
||||
if (observedSkill.SpecialSkill == Skills.None || observedSpecialInfo == null)
|
||||
{
|
||||
skillLine = $"<font color='{primaryColor}'>{primaryName}</font>";
|
||||
skillForColor = observedSkill.Skill;
|
||||
}
|
||||
else
|
||||
{
|
||||
string specialName = player.GetSkillName(observedSpecialInfo.Skill);
|
||||
skillLine = $"<font color='{observedSpecialInfo.Color}'>{specialName}({primaryName})</font>";
|
||||
// The special skill's color is what wraps the WHOLE combined line above,
|
||||
// not the primary skill's - matches that for the Panorama rarity class too.
|
||||
skillForColor = observedSpecialInfo.Skill;
|
||||
}
|
||||
|
||||
if (observedSkill.Skill != Skills.None && !string.IsNullOrEmpty(observedSkill.PrintHTML))
|
||||
|
|
@ -194,7 +221,7 @@ namespace src.player
|
|||
|
||||
if (string.IsNullOrEmpty(skillLine)) return;
|
||||
|
||||
Event.UpdateSkillHUD(player, skillPlayer, infoLine, skillLine, remainingLine, isDescription);
|
||||
Event.UpdateSkillHUD(player, skillPlayer, infoLine, skillLine, remainingLine, isDescription, skillForColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -620,7 +620,7 @@ namespace src.player
|
|||
UpdateSkillHudExpired(skillPlayer, randomSkill.Skill);
|
||||
assigned++;
|
||||
|
||||
if (randomSkill.Display)
|
||||
if (randomSkill.Display && Config.LoadedConfig.YourSkillChatInfo)
|
||||
Instance?.AddTimer(.6f, () =>
|
||||
{
|
||||
var descTarget = Utilities.GetPlayerFromIndex((int)playerIndex);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ namespace src.player.skills
|
|||
|
||||
string remainingLine = cooldown != 0
|
||||
? $"{player.GetTranslation("hud_info", $"<font color='#FF0000'>{cooldown}</font>")}"
|
||||
: $"<font color='#{(skillInfo == null || skillInfo.Count == 0 ? "FF0000" : "00FF00")}'>{(skillInfo == null ? 0 : skillInfo.Count)}/{SkillsInfo.GetValue<int>(skillName, "healthShotLimit")}</font>";
|
||||
: $"{player.GetSkillDescription(skillName)}<br><font color='#{(skillInfo == null || skillInfo.Count == 0 ? "FF0000" : "00FF00")}'>{(skillInfo == null ? 0 : skillInfo.Count)}/{SkillsInfo.GetValue<int>(skillName, "healthShotLimit")}</font>";
|
||||
|
||||
playerInfo.PrintHTML = remainingLine;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue