12 Commits

Author SHA1 Message Date
nocode e0a5424909 feat(bladw_warn): add warning system configuration
Create initial configuration file for the warning system with:
- Predefined warning reasons (RDM, VDM, RP violations, etc.)
- Chat announcement settings (enabled by default, scoped to target)
- Staff group definitions for permission levels
2026-07-20 23:29:53 +02:00
nocode 4e436a05c3 feat(storage): add warn data persistence layer
Implement server-side storage module for managing player warnings.

Provides functionality to:
- Load and save warnings from/to JSON file
- Retrieve warnings for a specific player
- Add new warnings with issuer information
- Remove individual warnings by index
- Clear all warnings for a player

Warnings are stored with issuer, issuerid, timestamp, reason, and comment.
2026-07-20 23:29:48 +02:00
nocode 63e35a534c feat(privileges): add privilege management for admin mods
Implement privilege registration and access checking for SAM, ULX, and FAdmin admin mods.

Provides:
- Registration of bladw_warn privilege with supported admin mods
- BladW.Warn.HasAccess() function to check player permissions
- Fallback to IsAdmin() if no admin mod is detected
2026-07-20 23:29:44 +02:00
nocode fad2323ab8 feat(networkstring): add network strings for warn system communication
Initialize network strings for bladw_warn system to enable communication between client and server for opening menu, syncing player warnings, adding/removing warnings, clearing records, and announcing warnings in chat.
2026-07-20 23:29:39 +02:00
nocode 6c95e7ed6f feat(bladw_warn): add server-side warning system functions
Implement core server-side functionality for the warning system including:
- SyncTo: synchronizes connected players and their warnings to a client
- OpenFor: opens the warning menu for authorized players
- Chat commands: /warns, !warns, /warn to open the menu
- Net receivers for adding, removing, and clearing warnings
- Chat announcement system for new warnings with configurable scope (all, target, staff)
- Access control checks on all admin operations
2026-07-20 23:29:35 +02:00
nocode 9ab34fe802 feat(client): add main warning menu interface with player list and history
Implement comprehensive client-side menu for the warning system including:
- Responsive player list with search functionality
- Warning history display with details
- Modal dialog for applying warnings with reason selection
- Custom styling with color palette and rounded corners
- Font Awesome icon integration
- Dynamic font scaling based on screen resolution
- Network communication for warning actions
2026-07-20 23:29:27 +02:00
nocode c0a3e57a89 build(fonts): add fontawesome solid 900 font file 2026-07-20 23:29:20 +02:00
nocode 41cc85a9e5 feat(bladWinRpMenu_loader): add support for pattern groups with alternatives syntax
Implement pattern group expansion allowing alternatives separated by '+' within parentheses.

Examples:
- "bladw_(ann*+warn)" expands to ["bladw_ann*", "bladw_warn"]
- Supports multiple groups with recursive Cartesian product expansion
- Refactor matching logic into separate Matcher() function for clarity
- Update root pattern in config from "bladw_announcement*" to "bladw_(ann*+warn)"
2026-07-20 23:29:10 +02:00
nocode 448baba2c1 feat(announcement): add announcement_theme configuration option
Add new configuration option to control the announcement border color theme.

Supports two theme options:
- "blue": blue border color
- "white": white border color (default)

This allows customization of the announcement appearance without code modification.
2026-07-20 18:23:53 +02:00
nocode e794c49c84 feat(announcement_menu): add theme-aware outer border color for loader style
introduce OuterCol() function to dynamically determine the outer border color based on the announcement theme configuration (white or blue). update the loader style border to use this function instead of hardcoded white color, allowing theme consistency across the announcement menu.
2026-07-20 18:23:49 +02:00
nocode f9773bd8ba feat(admin_menu): add theme-based outer border color configuration
Introduce OuterCol() function to dynamically set the outer border color based on the announcement_theme configuration. The border will be white when theme is set to "white", otherwise it defaults to the accent color. This allows for better visual customization of the admin menu appearance.
2026-07-20 18:23:44 +02:00
nocode 082ff3eedd chore(loader): enable bladw_announcement module loading
Change root configuration from "none" (disabled) to "bladw_announcement*" to enable automatic loading of announcement modules.
2026-07-20 18:23:30 +02:00
11 changed files with 827 additions and 11 deletions
+46 -8
View File
@@ -10,8 +10,10 @@ NoCode.config = {
-- • sans wildcard -> nom exact d'UN dossier : "bladw_announcement" -- • sans wildcard -> nom exact d'UN dossier : "bladw_announcement"
-- • avec wildcard -> glob, charge TOUS ceux qui matchent : -- • avec wildcard -> glob, charge TOUS ceux qui matchent :
-- "bladw_*" (préfixe) / "*_menu" (suffixe) / "bladw_*_v2" (milieu) -- "bladw_*" (préfixe) / "*_menu" (suffixe) / "bladw_*_v2" (milieu)
-- • groupes "(a+b)" -> alternatives séparées par + :
-- "bladw_(ann*+warn)" = bladw_ann* OU bladw_warn (les alt peuvent avoir des *)
-- • "none" (ou vide/nil) -> loader désactivé, rien n'est chargé -- • "none" (ou vide/nil) -> loader désactivé, rien n'est chargé
root = "none", root = "bladw_(ann*+warn)",
-- Dossiers de bibliothèques : seulement envoyés au client (AddCSLuaFile), -- Dossiers de bibliothèques : seulement envoyés au client (AddCSLuaFile),
-- jamais auto-inclus. Ils sont chargés à la demande via include("lib/..."). -- jamais auto-inclus. Ils sont chargés à la demande via include("lib/...").
libs = { "lib" }, libs = { "lib" },
@@ -237,16 +239,52 @@ local function GlobToPattern(glob)
return "^" .. EscapePattern(glob):gsub("%%%*", ".*") .. "$" return "^" .. EscapePattern(glob):gsub("%%%*", ".*") .. "$"
end end
--- Étend les groupes "(a+b)" en plusieurs motifs plats.
-- "bladw_(ann*+warn)" -> { "bladw_ann*", "bladw_warn" }
-- Gère plusieurs groupes (produit cartésien) via récursion sur le suffixe.
local function ExpandPattern(pattern)
local open = pattern:find("(", 1, true)
if not open then return { pattern } end
local close = pattern:find(")", open + 1, true)
if not close then return { pattern } end -- parenthèse non fermée -> littéral
local prefix = pattern:sub(1, open - 1)
local group = pattern:sub(open + 1, close - 1)
local suffix = pattern:sub(close + 1)
local out = {}
for _, alt in ipairs(string.Explode("+", group)) do
for _, exp in ipairs(ExpandPattern(prefix .. alt .. suffix)) do
out[#out + 1] = exp
end
end
return out
end
--- Construit un testeur (dossier -> bool) pour un motif plat (avec ou sans "*").
local function Matcher(p)
if p:find("*", 1, true) then
local lua_pat = GlobToPattern(p)
return function(d) return d:match(lua_pat) ~= nil end
end
return function(d) return d == p end
end
local function FindRootFolders(pattern) local function FindRootFolders(pattern)
local roots = {} local roots = {}
-- Construit la fonction de test selon la présence ou non d'un wildcard. -- On étend les groupes puis on teste chaque motif : un dossier matche si l'un d'eux matche.
local matches local matchers = {}
if pattern:find("*", 1, true) then for _, p in ipairs(ExpandPattern(pattern)) do
local lua_pat = GlobToPattern(pattern) matchers[#matchers + 1] = Matcher(p)
matches = function(d) return d:match(lua_pat) ~= nil end end
else
matches = function(d) return d == pattern end local function matches(d)
for _, m in ipairs(matchers) do
if m(d) then return true end
end
return false
end end
local _, dirs1 = file.Find("*", "LUA") local _, dirs1 = file.Find("*", "LUA")
@@ -16,6 +16,11 @@ local COL_MUTED = Color(160, 160, 160)
local COL_WARN = Color(255, 200, 50) local COL_WARN = Color(255, 200, 50)
local COL_LINE = Color(255, 255, 255, 60) local COL_LINE = Color(255, 255, 255, 60)
-- couleur des contours extérieurs selon la config ("blue" ou "white")
local function OuterCol()
return (BladW.Announcement.Conf or {}).announcement_theme == "white" and COL_TEXT or COL_ACCENT
end
-- nb de caractères d'un champ (UTF-8, un accent = 1) -- nb de caractères d'un champ (UTF-8, un accent = 1)
local function EntryLen(entry) local function EntryLen(entry)
local v = entry:GetValue() local v = entry:GetValue()
@@ -89,7 +94,7 @@ local function OpenAAdminMenu()
frame.Paint = function(self, w, h) frame.Paint = function(self, w, h)
rndx.Draw(20, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR) rndx.Draw(20, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
rndx.Draw(20, 0, 0, w, h, COL_BG, rndx.SHAPE_FIGMA) rndx.Draw(20, 0, 0, w, h, COL_BG, rndx.SHAPE_FIGMA)
rndx.DrawOutlined(20, 0, 0, w, h, COL_TEXT, 3, rndx.SHAPE_FIGMA) rndx.DrawOutlined(20, 0, 0, w, h, OuterCol(), 3, rndx.SHAPE_FIGMA)
rndx.DrawMaterial(0, w * 0.5 - 30, 20, 60, 60, COL_TEXT, logo) rndx.DrawMaterial(0, w * 0.5 - 30, 20, 60, 60, COL_TEXT, logo)
draw.SimpleText("Envoyer une annonce", "bladw_text", w * 0.5, 94, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) draw.SimpleText("Envoyer une annonce", "bladw_text", w * 0.5, 94, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
@@ -10,6 +10,11 @@ local COL_WHITE = Color(255, 255, 255)
local COL_BG = Color(0, 0, 0, 200) local COL_BG = Color(0, 0, 0, 200)
local COL_TRACK = Color(54, 54, 54, 200) local COL_TRACK = Color(54, 54, 54, 200)
-- couleur des contours extérieurs selon la config ("blue" ou "white")
local function OuterCol()
return (BladW.Announcement.Conf or {}).announcement_theme == "white" and COL_WHITE or COL_ACCENT
end
-- fonts responsives (refaites quand la résolution change) -- fonts responsives (refaites quand la résolution change)
local function AnnScale() local function AnnScale()
return math.min(ScrW() / 1920, ScrH() / 1080) return math.min(ScrW() / 1920, ScrH() / 1080)
@@ -203,9 +208,9 @@ hook.Add("HUDPaint", "BladeWin:Announcement", function()
rndx.Draw(radius, boxX, boxY, boxW, boxH, nil, rndx.SHAPE_FIGMA + rndx.BLUR) rndx.Draw(radius, boxX, boxY, boxW, boxH, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
rndx.Draw(radius, boxX, boxY, boxW, boxH, COL_BG, rndx.SHAPE_FIGMA) rndx.Draw(radius, boxX, boxY, boxW, boxH, COL_BG, rndx.SHAPE_FIGMA)
-- le loader garde son contour blanc (le simple lui a sa jauge sur le bord) -- le loader garde un contour fixe (couleur selon le thème config ; le simple a sa jauge sur le bord)
if style == "loader" then if style == "loader" then
rndx.DrawOutlined(radius, boxX, boxY, boxW, boxH, COL_WHITE, 3 * s, rndx.SHAPE_FIGMA) rndx.DrawOutlined(radius, boxX, boxY, boxW, boxH, OuterCol(), 3 * s, rndx.SHAPE_FIGMA)
end end
-- contenu empilé et centré -- contenu empilé et centré
@@ -4,6 +4,7 @@ BladW.Announcement.Conf = BladW.Announcement.Conf or {}
BladW.Announcement.Conf = { BladW.Announcement.Conf = {
announcement_style = "simple", -- "simple" ou "loader" announcement_style = "simple", -- "simple" ou "loader"
announcement_theme = "white", -- couleur des contours extérieurs : "blue" ou "white"
announcement_duration = 10, -- durée d'affichage en secondes announcement_duration = 10, -- durée d'affichage en secondes
announcement_max_length = 80, -- max de caractères pour le message announcement_max_length = 80, -- max de caractères pour le message
announcement_sound = "announce.mp3", -- son à l'apparition ("" = rien), fichier dans sound/ | ex : garrysmod/content_downloaded.wav announcement_sound = "announce.mp3", -- son à l'apparition ("" = rien), fichier dans sound/ | ex : garrysmod/content_downloaded.wav
+529
View File
@@ -0,0 +1,529 @@
local rndx = include("lib/rndx.lua")
local logo = Material("nocode/announcement/logo.png")
BladW = BladW or {}
BladW.Warn = BladW.Warn or {}
BladW.Warn.List = BladW.Warn.List or {}
-- palette
local COL_BG = Color(24, 24, 28)
local COL_PANEL = Color(38, 38, 44)
local COL_INPUT = Color(30, 30, 35)
local COL_LINE = Color(255, 255, 255, 22)
local COL_TEXT = Color(236, 236, 240)
local COL_MUTED = Color(150, 150, 160)
local COL_BLUE = Color(74, 108, 224)
local COL_RED = Color(226, 76, 76)
local COL_GREEN = Color(45, 170, 96)
local COL_ORANGE = Color(232, 168, 32)
local COL_SEL = Color(50, 50, 60)
local COL_BAR = Color(92, 92, 104) -- petit bout à gauche des joueurs (non sélectionné)
local COL_BADGE = Color(22, 22, 26) -- fond du badge "warns actifs" (gris foncé)
local function Lighten(c, a)
a = a or 22
return Color(math.min(c.r + a, 255), math.min(c.g + a, 255), math.min(c.b + a, 255), c.a)
end
-- ============================================================
-- FontAwesome (vrais glyphes)
-- Le .ttf doit être chargé côté client : dépose un fa-solid-900.ttf
-- dans resource/fonts/. Change FA_FONT si ta police a un autre nom
-- de famille, et les codepoints si ta version de FA diffère.
-- ============================================================
local FA_FONT = "Font Awesome 6 Free Solid" -- nom de famille Windows (name ID 1), pas le typo "Font Awesome 6 Free"
local FA = {
search = 0xf002, -- magnifying-glass
shield = 0xf3ed, -- shield-halved (shield-alt en FA5)
card = 0xf2c2, -- id-card
copy = 0xf0c5, -- copy
trash = 0xf1f8, -- trash
}
local function FAIcon(cp, font, x, y, col, ax, ay)
draw.SimpleText(utf8.char(cp), font or "bw_fa", x, y, col, ax or TEXT_ALIGN_CENTER, ay or TEXT_ALIGN_CENTER)
end
-- fonts responsives
local FS = 1
local function BuildFonts()
FS = math.Clamp(ScrH() / 1080, 0.7, 1.5)
surface.CreateFont("bw_title", { font = "Poppins-Bold", size = math.Round(24 * FS), weight = 700, antialias = true })
surface.CreateFont("bw_big", { font = "Poppins-SemiBold", size = math.Round(19 * FS), weight = 600, antialias = true })
surface.CreateFont("bw_med", { font = "Poppins-Medium", size = math.Round(15 * FS), weight = 500, antialias = true })
surface.CreateFont("bw_bold", { font = "Poppins-Bold", size = math.Round(15 * FS), weight = 700, antialias = true })
surface.CreateFont("bw_small", { font = "Poppins-Regular", size = math.Round(13 * FS), weight = 400, antialias = true })
surface.CreateFont("bw_fa", { font = FA_FONT, size = math.Round(15 * FS), weight = 900, extended = true })
surface.CreateFont("bw_fa_lg", { font = FA_FONT, size = math.Round(19 * FS), weight = 900, extended = true })
end
BuildFonts()
hook.Add("OnScreenSizeChanged", "bw_warn_fonts", BuildFonts)
-- temps relatif ("2 mois", "3 j"...)
local function RelTime(t)
local d = os.time() - (t or 0)
if d < 60 then return "à l'instant"
elseif d < 3600 then return math.floor(d / 60) .. " min"
elseif d < 86400 then return math.floor(d / 3600) .. " h"
elseif d < 2592000 then return math.floor(d / 86400) .. " j"
elseif d < 31536000 then return math.floor(d / 2592000) .. " mois"
else return math.floor(d / 31536000) .. " an(s)" end
end
local function Btn(parent, label, col, font)
local b = vgui.Create("DButton", parent)
b:SetText("")
b.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and Lighten(col) or col, rndx.SHAPE_FIGMA)
draw.SimpleText(label, font or "bw_med", w * 0.5, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
return b
end
local function StyleEntry(e, ph, multiline)
e:SetFont("bw_med")
e:SetTextColor(COL_TEXT)
e:SetPlaceholderText(ph or "")
e:SetPaintBackground(false)
if multiline then e:SetMultiline(true) end
e.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
self:DrawTextEntryText(COL_TEXT, COL_BLUE, COL_TEXT)
end
end
local function StyleScroll(sp)
local bar = sp:GetVBar()
bar:SetWide(8)
bar.Paint = function() end
bar.btnUp.Paint = function() end
bar.btnDown.Paint = function() end
bar.btnGrip.Paint = function(self, w, h) rndx.Draw(4, 0, 0, w, h, COL_LINE, rndx.SHAPE_FIGMA) end
end
local function GetEntry(sid)
for _, e in ipairs(BladW.Warn.List) do
if e.sid == sid then return e end
end
end
-- ============================================================
-- Modal « Appliquer un warn »
-- ============================================================
local function OpenWarnModal(sid)
local reasons = (BladW.Warn.Conf or {}).reasons or {}
local chosen, chosenLabel = nil, "Choisissez une raison..."
local m = vgui.Create("DFrame")
m:SetSize(math.min(ScrW() * 0.42, 580), math.min(ScrH() * 0.5, 400))
m:Center()
m:SetTitle("")
m:ShowCloseButton(false)
m:MakePopup()
m:DockPadding(30, 66, 30, 26)
m.Paint = function(self, w, h)
rndx.Draw(16, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
rndx.DrawOutlined(16, 0, 0, w, h, COL_LINE, 2, rndx.SHAPE_FIGMA)
draw.SimpleText("Appliquer un warn", "bw_title", w * 0.5, 36, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
local mclose = vgui.Create("DButton", m)
mclose:SetSize(32, 32)
mclose:SetText("")
mclose.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and COL_RED or COL_BG, rndx.SHAPE_FIGMA)
draw.SimpleText("X", "bw_med", w * 0.5, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
mclose.DoClick = function() m:Remove() end
m.PerformLayout = function(self, w, h) mclose:SetPos(w - 42, 14) end
-- dropdown des raisons
local rdd = vgui.Create("DButton", m)
rdd:Dock(TOP)
rdd:SetTall(46)
rdd:SetText("")
rdd.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 2, rndx.SHAPE_FIGMA)
draw.SimpleText(chosenLabel, "bw_med", 14, h * 0.5, chosen and COL_TEXT or COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
draw.NoTexture()
surface.SetDrawColor(COL_MUTED)
local ax = w - 26
surface.DrawPoly({ { x = ax, y = h * 0.5 - 2 }, { x = ax + 10, y = h * 0.5 - 2 }, { x = ax + 5, y = h * 0.5 + 4 } })
end
rdd.DoClick = function(self)
local menu = DermaMenu()
menu:SetMinimumWidth(self:GetWide())
menu.Paint = function(s, w, h)
rndx.Draw(8, 0, 0, w, h, Color(20, 20, 24, 252), rndx.SHAPE_FIGMA)
rndx.DrawOutlined(8, 0, 0, w, h, COL_LINE, 1, rndx.SHAPE_FIGMA)
end
for _, r in ipairs(reasons) do
local o = menu:AddOption(r, function() chosen, chosenLabel = r, r end)
o:SetTall(30)
o:SetText("")
o.Paint = function(s, w, h)
if s:IsHovered() then rndx.Draw(6, 3, 1, w - 6, h - 2, COL_BLUE, rndx.SHAPE_FIGMA) end
draw.SimpleText(r, "bw_med", 12, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
end
local mx, my = self:LocalToScreen(0, self:GetTall() + 2)
menu:Open(mx, my)
end
-- commentaire du staff
local clabel = vgui.Create("DLabel", m)
clabel:Dock(TOP)
clabel:DockMargin(0, 18, 0, 6)
clabel:SetTall(20)
clabel:SetFont("bw_med")
clabel:SetTextColor(COL_TEXT)
clabel:SetText("Commentaires du staff :")
local comment = vgui.Create("DTextEntry", m)
comment:Dock(TOP)
comment:SetTall(90)
StyleEntry(comment, "Joueur a surveiller...", true)
-- bouton Avertir
local warnBtn = Btn(m, "Avertir", COL_GREEN, "bw_big")
warnBtn:Dock(BOTTOM)
warnBtn:SetTall(46)
warnBtn:DockMargin(90, 16, 90, 0)
warnBtn.DoClick = function()
if not chosen then
surface.PlaySound("buttons/button10.wav")
return
end
net.Start("bladw_warn_add")
net.WriteString(sid)
net.WriteString(chosen)
net.WriteString(comment:GetValue() or "")
net.SendToServer()
m:Remove()
end
end
-- ============================================================
-- Menu principal
-- ============================================================
function BladW.Warn.Open()
if IsValid(BladW.Warn.Frame) then BladW.Warn.Frame:Remove() end
local W = math.min(ScrW() * 0.72, 1180)
local H = math.min(ScrH() * 0.80, 750)
local frame = vgui.Create("DFrame")
BladW.Warn.Frame = frame
frame:SetSize(W, H)
frame:Center()
frame:SetTitle("")
frame:ShowCloseButton(false)
frame:MakePopup()
frame:DockPadding(16, 16, 16, 16)
frame.Paint = function(self, w, h)
local lw = w * 0.3 + 24 -- fin de la zone gauche (alignée sur la colonne joueurs)
-- côté droit : blur + voile (assez sombre mais on voit le fond)
rndx.Draw(18, 0, 0, w, h, nil, rndx.SHAPE_FIGMA + rndx.BLUR)
rndx.Draw(18, 0, 0, w, h, Color(24, 24, 28, 200), rndx.SHAPE_FIGMA)
-- côté gauche : couleur pleine (coins arrondis à gauche, carré à droite)
rndx.Draw(18, 0, 0, lw, h, COL_BG, rndx.SHAPE_FIGMA + rndx.NO_TR + rndx.NO_BR)
-- bordure blanche (comme les annonces)
rndx.DrawOutlined(18, 0, 0, w, h, Color(255, 255, 255), 3, rndx.SHAPE_FIGMA)
end
local selectedSID
-- ===== colonne gauche =====
local left = vgui.Create("DPanel", frame)
left:Dock(LEFT)
left:SetWide(W * 0.3)
left:DockMargin(0, 0, 16, 0)
left:SetPaintBackground(false)
local lhead = vgui.Create("DPanel", left)
lhead:Dock(TOP)
lhead:SetTall(46)
lhead:SetPaintBackground(false)
lhead.Paint = function(self, w, h)
rndx.DrawMaterial(0, 0, (h - 42) * 0.5, 42, 42, color_white, logo)
draw.SimpleText("Joueurs", "bw_title", 54, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
local searchBox = vgui.Create("DPanel", left)
searchBox:Dock(TOP)
searchBox:DockMargin(0, 8, 0, 10)
searchBox:SetTall(40)
searchBox:SetPaintBackground(false)
searchBox.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, COL_INPUT, rndx.SHAPE_FIGMA)
FAIcon(FA.search, "bw_fa", 22, h * 0.5, COL_MUTED)
end
local search = vgui.Create("DTextEntry", searchBox)
search:Dock(FILL)
search:DockMargin(38, 0, 10, 0)
search:SetFont("bw_med")
search:SetTextColor(COL_TEXT)
search:SetPlaceholderText("Rechercher un joueur")
search:SetPaintBackground(false)
search.Paint = function(self, w, h)
self:DrawTextEntryText(COL_TEXT, COL_BLUE, COL_TEXT)
end
local plist = vgui.Create("DScrollPanel", left)
plist:Dock(FILL)
StyleScroll(plist)
-- ===== colonne droite =====
local right = vgui.Create("DPanel", frame)
right:Dock(FILL)
right:SetPaintBackground(false)
local rhead = vgui.Create("DPanel", right)
rhead:Dock(TOP)
rhead:SetTall(58)
rhead:SetPaintBackground(false)
rhead.Paint = function(self, w, h)
draw.SimpleText("Historique", "bw_title", w * 0.5, 16, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
FAIcon(FA.card, "bw_fa_lg", w * 0.5, 44, COL_TEXT)
end
local close = vgui.Create("DButton", rhead)
close:Dock(RIGHT)
close:SetWide(40)
close:SetText("")
close.Paint = function(self, w, h)
local y = (h - 34) * 0.5
rndx.Draw(8, w - 34, y, 34, 34, self:IsHovered() and COL_RED or COL_PANEL, rndx.SHAPE_FIGMA)
draw.SimpleText("X", "bw_big", w - 17, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
close.DoClick = function() frame:Remove() end
local content = vgui.Create("DPanel", right)
content:Dock(FILL)
content:DockMargin(0, 8, 0, 0)
content:SetPaintBackground(false)
-- ===== refresh =====
local RefreshRight
local function RefreshPlayers()
plist:Clear()
local filter = string.lower(search:GetValue() or "")
for _, e in ipairs(BladW.Warn.List) do
if filter == "" or string.find(string.lower(e.name), filter, 1, true) then
local entry = e
local row = plist:Add("DButton")
row:Dock(TOP)
row:DockMargin(0, 0, 8, 8)
row:SetTall(52)
row:SetText("")
row.Paint = function(self, w, h)
local sel = selectedSID == entry.sid
rndx.Draw(8, 0, 0, w, h, sel and COL_SEL or COL_PANEL, rndx.SHAPE_FIGMA)
-- petit bout à gauche : bleu si sélectionné, gris clair sinon (coins gauche arrondis)
rndx.Draw(8, 0, 0, 5, h, sel and COL_BLUE or COL_BAR, rndx.SHAPE_FIGMA + rndx.NO_TR + rndx.NO_BR)
draw.SimpleText(entry.name, "bw_bold", 60, h * 0.5, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
if entry.staff then
FAIcon(FA.shield, "bw_fa", w - 28, h * 0.5, COL_ORANGE)
end
end
row.DoClick = function() selectedSID = entry.sid RefreshRight() end
local av = vgui.Create("AvatarImage", row)
av:SetSize(34, 34)
av:SetPos(14, 9)
av:SetSteamID(entry.sid, 64)
av:SetMouseInputEnabled(false)
end
end
end
function RefreshRight()
content:Clear()
local e = selectedSID and GetEntry(selectedSID)
if not e then
local ph = vgui.Create("DPanel", content)
ph:Dock(FILL)
ph:SetPaintBackground(false)
ph.Paint = function(self, w, h)
draw.SimpleText("Sélectionne un joueur à gauche", "bw_med", w * 0.5, h * 0.4, COL_MUTED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
return
end
-- carte joueur
local card = vgui.Create("DPanel", content)
card:Dock(TOP)
card:SetTall(90)
card:SetPaintBackground(false)
card.Paint = function(self, w, h)
rndx.Draw(12, 0, 0, w, h, COL_PANEL, rndx.SHAPE_FIGMA)
draw.SimpleText(e.name, "bw_big", 96, 30, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
draw.SimpleText("Steam ID :", "bw_small", 96, 56, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
local n = #e.warns
local txt = n .. (n > 1 and " Warns actifs" or " Warn actif")
surface.SetFont("bw_bold")
local bw = surface.GetTextSize(txt) + 30
rndx.Draw(6, w * 0.5 - bw * 0.5, h * 0.5 - 16, bw, 32, COL_BADGE, rndx.SHAPE_FIGMA)
draw.SimpleText(txt, "bw_bold", w * 0.5, h * 0.5, COL_RED, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
local av = vgui.Create("AvatarImage", card)
av:SetSize(56, 56)
av:SetPos(22, 17)
av:SetSteamID(e.sid, 64)
av:SetMouseInputEnabled(false)
-- bouton copie du SteamID
local copyBtn = vgui.Create("DButton", card)
copyBtn:SetText("")
copyBtn:SetSize(16, 16)
copyBtn:SetTooltip("Copier le SteamID")
copyBtn.Paint = function(self, w, h)
FAIcon(FA.copy, "bw_fa", w * 0.5, h * 0.5, self:IsHovered() and COL_TEXT or COL_MUTED)
end
copyBtn.DoClick = function() SetClipboardText(e.sid) end
card.PerformLayout = function(self, w, h)
surface.SetFont("bw_small")
local tw = surface.GetTextSize("Steam ID :")
copyBtn:SetPos(96 + tw + 8, 49)
end
-- ligne « Ajouter un warn »
local addRow = vgui.Create("DPanel", content)
addRow:Dock(TOP)
addRow:SetTall(64)
addRow:DockMargin(0, 10, 0, 4)
addRow:SetPaintBackground(false)
addRow.Paint = function(self, w, h)
surface.SetDrawColor(COL_LINE)
surface.DrawRect(0, h * 0.5, w * 0.5 - 100, 1)
surface.DrawRect(w * 0.5 + 100, h * 0.5, w * 0.5 - 100, 1)
end
local addBtn = Btn(addRow, "Ajouter un warn", COL_BLUE, "bw_big")
addBtn:SetSize(190, 42)
addRow.PerformLayout = function(self, w, h) addBtn:SetPos(w * 0.5 - 95, h * 0.5 - 21) end
addBtn.DoClick = function() OpenWarnModal(e.sid) end
-- ligne « Vider le casier » (en bas)
local clearRow = vgui.Create("DPanel", content)
clearRow:Dock(BOTTOM)
clearRow:SetTall(54)
clearRow:SetPaintBackground(false)
local clearBtn = Btn(clearRow, "Vider le casier", COL_ORANGE, "bw_med")
clearBtn:SetSize(190, 40)
clearBtn.Paint = function(self, w, h)
rndx.Draw(8, 0, 0, w, h, self:IsHovered() and Lighten(COL_ORANGE) or COL_ORANGE, rndx.SHAPE_FIGMA)
FAIcon(FA.trash, "bw_fa", 32, h * 0.5, COL_TEXT)
draw.SimpleText("Vider le casier", "bw_med", w * 0.5 + 14, h * 0.5, COL_TEXT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
clearRow.PerformLayout = function(self, w, h) clearBtn:SetPos(w - 190, h - 46) end
clearBtn.DoClick = function()
if #e.warns == 0 then return end
net.Start("bladw_warn_clear")
net.WriteString(e.sid)
net.SendToServer()
end
-- liste des warns
local warns = vgui.Create("DScrollPanel", content)
warns:Dock(FILL)
warns:DockMargin(0, 6, 0, 6)
StyleScroll(warns)
for i, w in ipairs(e.warns) do
local idx, warn = i, w
local wc = warns:Add("DPanel")
wc:Dock(TOP)
wc:DockMargin(0, 0, 8, 10)
wc:SetTall(116)
wc:SetPaintBackground(false)
wc.Paint = function(self, cw, ch)
rndx.Draw(10, 0, 0, cw, ch, COL_PANEL, rndx.SHAPE_FIGMA)
draw.SimpleText("Warn émis par :", "bw_med", 16, 16, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
draw.SimpleText(warn.issuer ~= "" and warn.issuer or "?", "bw_med", 168, 16, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
draw.SimpleText("Date :", "bw_med", 16, 40, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
if warn.time and warn.time > 0 then
local d = ("(%s) %s"):format(RelTime(warn.time), os.date("%d/%m/%Y, %I:%M %p", warn.time))
draw.SimpleText(d, "bw_med", 168, 40, COL_MUTED, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
end
draw.SimpleText("Raison :", "bw_med", 16, 64, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
rndx.Draw(6, 14, ch - 40, cw - 28, 30, COL_INPUT, rndx.SHAPE_FIGMA)
draw.SimpleText(warn.reason or "", "bw_small", 26, ch - 25, COL_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
end
local del = Btn(wc, "Supprimer", COL_RED, "bw_med")
del:SetSize(150, 36)
wc.PerformLayout = function(self, cw, ch) del:SetPos(cw - 164, 14) end
del.DoClick = function()
net.Start("bladw_warn_remove")
net.WriteString(e.sid)
net.WriteUInt(idx, 8)
net.SendToServer()
end
end
end
frame.RefreshAll = function()
RefreshPlayers()
RefreshRight()
end
search.OnChange = function() RefreshPlayers() end
RefreshPlayers()
RefreshRight()
end
-- ============================================================
-- Réseau
-- ============================================================
net.Receive("bladw_warn_open", function()
BladW.Warn.Open()
end)
-- annonce d'un warn dans le chat de base
net.Receive("bladw_warn_chat", function()
local target = net.ReadString()
local admin = net.ReadString()
local reason = net.ReadString()
chat.AddText(
COL_ORANGE, "[Warn] ",
COL_TEXT, target,
COL_MUTED, " a été averti par ",
COL_TEXT, admin,
COL_MUTED, "",
COL_RED, reason
)
end)
net.Receive("bladw_warn_sync", function()
local list = {}
local n = net.ReadUInt(8)
for i = 1, n do
local e = { name = net.ReadString(), sid = net.ReadString(), staff = net.ReadBool(), warns = {} }
local wn = net.ReadUInt(8)
for j = 1, wn do
e.warns[j] = {
issuer = net.ReadString(),
time = net.ReadUInt(32),
reason = net.ReadString(),
comment = net.ReadString(),
}
end
list[i] = e
end
BladW.Warn.List = list
if IsValid(BladW.Warn.Frame) and BladW.Warn.Frame.RefreshAll then
BladW.Warn.Frame.RefreshAll()
end
end)
+116
View File
@@ -0,0 +1,116 @@
BladW = BladW or {}
BladW.Warn = BladW.Warn or {}
-- envoie au client la liste des joueurs connectés + leurs warns
local function SyncTo(ply)
if not IsValid(ply) then return end
local players = player.GetAll()
net.Start("bladw_warn_sync")
net.WriteUInt(#players, 8)
for _, p in ipairs(players) do
local sid = p:SteamID64() or ""
local warns = BladW.Warn.Get(sid)
local isStaff = p:IsAdmin() or table.HasValue((BladW.Warn.Conf or {}).staff_groups or {}, p:GetUserGroup())
net.WriteString(p:Nick())
net.WriteString(sid)
net.WriteBool(isStaff)
net.WriteUInt(math.min(#warns, 255), 8)
for i = 1, math.min(#warns, 255) do
local w = warns[i]
net.WriteString(w.issuer or "")
net.WriteUInt(w.time or 0, 32)
net.WriteString(w.reason or "")
net.WriteString(w.comment or "")
end
end
net.Send(ply)
end
BladW.Warn.SyncTo = SyncTo
-- ouvre le menu pour un joueur autorisé
local function OpenFor(ply)
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
net.Start("bladw_warn_open")
net.Send(ply)
SyncTo(ply)
end
concommand.Add("bladw_warns", function(ply) OpenFor(ply) end)
hook.Add("PlayerSay", "BladW:WarnChatCmd", function(ply, text)
local cmd = string.lower(string.Trim(text))
if cmd ~= "/warns" and cmd ~= "!warns" and cmd ~= "/warn" then return end
OpenFor(ply)
return "" -- on retire la commande du chat
end)
-- un admin ajoute un warn
net.Receive("bladw_warn_add", function(len, ply)
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
local sid = net.ReadString()
local reason = string.sub(net.ReadString(), 1, 100)
local comment = string.sub(net.ReadString(), 1, 300)
if sid == "" or reason == "" then return end
BladW.Warn.AddWarn(sid, {
issuer = ply:Nick(),
issuerid = ply:SteamID64(),
time = os.time(),
reason = reason,
comment = comment,
})
-- annonce le warn dans le chat de base
local Conf = BladW.Warn.Conf or {}
if Conf.chat_announce ~= false then
local target = player.GetBySteamID64(sid)
local targetName = IsValid(target) and target:Nick() or sid
local recips
local scope = Conf.chat_scope or "all"
if scope == "target" then
recips = IsValid(target) and { target } or {}
elseif scope == "staff" then
recips = {}
for _, p in ipairs(player.GetAll()) do
if BladW.Warn.HasAccess(p) then recips[#recips + 1] = p end
end
end
-- recips == nil -> tout le monde ; sinon on n'envoie que si la liste n'est pas vide
if not recips or #recips > 0 then
net.Start("bladw_warn_chat")
net.WriteString(targetName)
net.WriteString(ply:Nick())
net.WriteString(reason)
if recips then net.Send(recips) else net.Broadcast() end
end
end
SyncTo(ply)
end)
-- un admin supprime un warn précis
net.Receive("bladw_warn_remove", function(len, ply)
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
local sid = net.ReadString()
local index = net.ReadUInt(8)
BladW.Warn.RemoveWarn(sid, index)
SyncTo(ply)
end)
-- un admin vide le casier d'un joueur
net.Receive("bladw_warn_clear", function(len, ply)
if not IsValid(ply) or not BladW.Warn.HasAccess(ply) then return end
local sid = net.ReadString()
BladW.Warn.ClearWarns(sid)
SyncTo(ply)
end)
@@ -0,0 +1,6 @@
util.AddNetworkString("bladw_warn_open") -- ouvre le menu chez le client
util.AddNetworkString("bladw_warn_sync") -- serveur -> client : joueurs + leurs warns
util.AddNetworkString("bladw_warn_add") -- client -> serveur : ajouter un warn
util.AddNetworkString("bladw_warn_remove") -- client -> serveur : supprimer un warn
util.AddNetworkString("bladw_warn_clear") -- client -> serveur : vider le casier
util.AddNetworkString("bladw_warn_chat") -- serveur -> clients : annonce du warn dans le chat
+40
View File
@@ -0,0 +1,40 @@
BladW = BladW or {}
BladW.Warn = BladW.Warn or {}
-- le droit géré par l'admin mod (SAM / ULX / FAdmin)
local PRIV = "bladw_warn"
BladW.Warn.Privilege = PRIV
-- enregistre le droit dans l'admin mod présent
local function Register()
if sam and sam.permissions and sam.permissions.add then
pcall(sam.permissions.add, PRIV, "BladW Warn", "admin")
end
if ULib and ULib.ucl and ULib.ucl.registerAccess then
pcall(ULib.ucl.registerAccess, PRIV, "admin", "Gérer les warns BladW", "BladW")
end
if FAdmin and FAdmin.Access and FAdmin.Access.AddPrivilege then
pcall(FAdmin.Access.AddPrivilege, PRIV, 2)
end
end
hook.Add("SAM.LoadPermissions", "BladW:WarnPriv", Register)
hook.Add("InitPostEntity", "BladW:WarnPriv", Register)
Register()
-- ce joueur peut-il gérer les warns ?
function BladW.Warn.HasAccess(ply)
if not IsValid(ply) then return false end
if sam and sam.player and sam.player.has_permission then
return sam.player.has_permission(ply, PRIV)
end
if ULib and ULib.ucl and ULib.ucl.query then
return ULib.ucl.query(ply, PRIV)
end
if FAdmin and FAdmin.Access and FAdmin.Access.PlayerHasPrivilege then
return FAdmin.Access.PlayerHasPrivilege(ply, PRIV)
end
return ply:IsAdmin()
end
+47
View File
@@ -0,0 +1,47 @@
BladW = BladW or {}
BladW.Warn = BladW.Warn or {}
-- stockage des warns : { [steamid64] = { { issuer, issuerid, time, reason, comment }, ... } }
local DIR = "bladw_warn"
local FILE = "bladw_warn/warns.json"
BladW.Warn.Data = BladW.Warn.Data or {}
function BladW.Warn.Save()
file.CreateDir(DIR)
file.Write(FILE, util.TableToJSON(BladW.Warn.Data))
end
function BladW.Warn.Load()
local raw = file.Read(FILE, "DATA")
BladW.Warn.Data = (raw and util.JSONToTable(raw)) or {}
end
function BladW.Warn.Get(sid)
return BladW.Warn.Data[sid] or {}
end
function BladW.Warn.AddWarn(sid, entry)
if not sid or sid == "" then return end
BladW.Warn.Data[sid] = BladW.Warn.Data[sid] or {}
table.insert(BladW.Warn.Data[sid], entry)
BladW.Warn.Save()
end
function BladW.Warn.RemoveWarn(sid, index)
local t = BladW.Warn.Data[sid]
if t and t[index] then
table.remove(t, index)
if #t == 0 then BladW.Warn.Data[sid] = nil end
BladW.Warn.Save()
return true
end
return false
end
function BladW.Warn.ClearWarns(sid)
BladW.Warn.Data[sid] = nil
BladW.Warn.Save()
end
BladW.Warn.Load()
+29
View File
@@ -0,0 +1,29 @@
BladW = BladW or {}
BladW.Warn = BladW.Warn or {}
BladW.Warn.Conf = BladW.Warn.Conf or {}
-- config des warns
BladW.Warn.Conf = {
-- les raisons proposées dans le menu déroulant du modal
reasons = {
"RDM",
"VDM",
"No Fear RP",
"Fail RP",
"Troll",
"Metagaming",
"Powergaming",
"Propos déplacés / insultes",
"Non-respect du staff",
"Publicité",
"Bug abuse",
},
-- annonce le warn dans le chat de base
chat_announce = true,
-- à qui : "all" (tout le monde) | "target" (juste l'averti) | "staff" (les admins)
chat_scope = "target",
-- groupes considérés comme "staff" (affiche le bouclier). IsAdmin() est toujours staff.
staff_groups = { "superadmin", "admin", "moderator", "modo", "helper", "support" },
}
Binary file not shown.