add sborka

This commit is contained in:
2026-03-31 10:27:04 +03:00
commit f5e5f56c84
2345 changed files with 382127 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
-- Garry's Mod Binder - Entry Point
AddCSLuaFile("binder/sh_lang.lua")
AddCSLuaFile("binder/core/sh_config.lua")
AddCSLuaFile("binder/cl_init.lua")
AddCSLuaFile("binder/vgui/cl_frame.lua")
AddCSLuaFile("binder/vgui/cl_profiles_tab.lua")
AddCSLuaFile("binder/vgui/cl_keybind_tab.lua")
AddCSLuaFile("binder/vgui/cl_radial_tab.lua")
AddCSLuaFile("binder/vgui/cl_radial_hud.lua")
AddCSLuaFile("binder/vgui/cl_settings_tab.lua")
if SERVER then
include("binder/sv_init.lua")
print("[Binder] Server loaded")
else
include("binder/cl_init.lua")
print("[Binder] Client loaded")
end

View File

@@ -0,0 +1,135 @@
-- Realistic Police Helix Sync
-- Created to restrict computer to FSB and sync wanted status with Military ID
local FSB_PODR_ID = 6
Realistic_Police = Realistic_Police or {}
-- New helper to get the job name, respecting Helix units (podrazdeneniya)
function Realistic_Police.GetPlayerJob(ply)
if not IsValid(ply) then return "" end
local char = ply.GetCharacter and ply:GetCharacter()
if not char then return team.GetName(ply:Team()) end
-- In this server, if the faction is FACTION_RUSSIAN, we check the unit ID
if char:GetFaction() == (FACTION_RUSSIAN or -1) then
local podrId = char:GetPodr()
if podrId == FSB_PODR_ID then
return "ФСБ «Вымпел»" -- Note: must match config exactly (2 spaces)
end
end
return team.GetName(ply:Team())
end
if SERVER then
local plyMeta = FindMetaTable("Player")
-- Define wanted status for players in Helix
function plyMeta:isWanted()
local char = self:GetCharacter()
if not char then return false end
return char:GetData("wanted", false)
end
function plyMeta:wanted(actor, reason, duration)
local char = self:GetCharacter()
if not char then return end
char:SetData("wanted", true)
char:SetData("wanted_reason", reason or "Розыск ФСБ")
-- Custom long-term wanted logic: we ignore duration and keep it until manual removal
net.Start("RealisticPolice:Open")
net.WriteString("Notify")
net.WriteString(self:Name() .. " теперь находится в розыске!")
net.Broadcast()
end
function plyMeta:unWanted(actor)
local char = self:GetCharacter()
if not char then return end
char:SetData("wanted", false)
char:SetData("wanted_reason", nil)
net.Start("RealisticPolice:Open")
net.WriteString("Notify")
net.WriteString(self:Name() .. " снят с розыска.")
net.Broadcast()
end
-- Hook into say commands if the police addon uses them
hook.Add("PlayerSay", "RealisticPolice_WantedCommands", function(ply, text)
local lowText = string.lower(text)
local isWanted = string.StartWith(lowText, "/wanted") or string.StartWith(lowText, "!wanted")
local isUnwanted = string.StartWith(lowText, "/unwanted") or string.StartWith(lowText, "!unwanted")
if isWanted or isUnwanted then
-- Permission check
local job = Realistic_Police.GetPlayerJob(ply)
if not Realistic_Police.OpenComputer[job] and not Realistic_Police.AdminRank[ply:GetUserGroup()] then
return
end
-- Parse Name and Reason
local cmd = isWanted and (string.StartWith(lowText, "/wanted") and "/wanted" or "!wanted") or (string.StartWith(lowText, "/unwanted") and "/unwanted" or "!unwanted")
local fullParams = string.Trim(string.sub(text, #cmd + 1))
local target = nil
local reason = ""
-- Find the player whose name matches the start of fullParams
for _, v in pairs(player.GetAll()) do
local name = v:Name()
if string.StartWith(fullParams, name) then
-- If multiple players match (e.g. "Ivan" and "Ivan Ivanov"), we want the longest match
if not target or #name > #target:Name() then
target = v
reason = string.Trim(string.sub(fullParams, #name + 1))
end
end
end
if target then
if isWanted then
target:wanted(ply, reason)
else
target:unWanted(ply)
end
return "" -- Hide from chat
end
end
end)
-- Robust server-side check for opening the computer
hook.Add("PlayerUse", "RealisticPolice_FSBRestrict", function(ply, ent)
if IsValid(ent) and ent:GetClass() == "realistic_police_computer" then
local job = Realistic_Police.GetPlayerJob(ply)
if not Realistic_Police.OpenComputer[job] and not Realistic_Police.AdminRank[ply:GetUserGroup()] then
if not (Realistic_Police.HackerJob and Realistic_Police.HackerJob[job]) then
Realistic_Police.SendNotify(ply, "Доступ к компьютеру разрешен только подразделению ФСБ!")
return false
end
end
end
end)
end
if CLIENT then
-- Override the computer open check to restrict to FSB
timer.Simple(1, function()
if not Realistic_Police then return end
local oldOpenMenu = Realistic_Police.OpenMenu
Realistic_Police.OpenMenu = function(ent, bool)
local job = Realistic_Police.GetPlayerJob(LocalPlayer())
if not Realistic_Police.OpenComputer[job] and not Realistic_Police.AdminRank[LocalPlayer():GetUserGroup()] then
if not (Realistic_Police.HackerJob and Realistic_Police.HackerJob[job]) then
RealisticPoliceNotify("Доступ запрещен. Только для подразделения ФСБ.")
return
end
end
oldOpenMenu(ent, bool)
end
end)
end

View File

@@ -0,0 +1,77 @@
if not SERVER then return end
-- Таблица групп, которым разрешён административный доступ к Physgun
local AdminGroups = {
["superadmin"] = true,
["super admin"] = true,
["owner"] = true,
["curator"] = true,
["sudo-curator"] = true,
["asist-sudo"] = true,
["admin"] = true,
["st.admin"] = true,
["projectteam"] = true,
["teh.admin"] = true,
["ivent"] = true,
["st.event"] = true,
["event"] = true,
["specadmin"] = true,
["assistant"] = true,
["disp"] = true,
}
-- Хук для Physgun: позволяем администраторам поднимать энтити и игроков
hook.Add("PhysgunPickup", "!!!!Admin_PhysgunPickup_Fix", function(ply, ent)
if not IsValid(ply) then return end
local group = ply:GetUserGroup()
if AdminGroups[group] then
-- Если это игрок, SAM уже имеет свою логику, но мы подтверждаем наше разрешение
if ent:IsPlayer() then
-- Запрещаем поднимать суперадминов если мы просто "админ" (опционально)
-- if group == "admin" and ent:IsSuperAdmin() then return false end
return true
end
-- Для всех остальных энтитей — разрешаем
return true
end
end)
-- Хук для ToolGun: позволяем администраторам использовать инструменты на защищённых объектах
hook.Add("CanTool", "!!!!Admin_CanTool_Fix", function(ply, tr, tool)
if IsValid(ply) and AdminGroups[ply:GetUserGroup()] then
return true
end
end)
-- Хук для Properties (ПКМ меню): позволяем администраторам видеть всё
hook.Add("CanProperty", "!!!!Admin_CanProperty_Fix", function(ply, property, ent)
if IsValid(ply) and AdminGroups[ply:GetUserGroup()] then
return true
end
end)
-- Хук для Unfreeze: позволяем администраторам размораживать объекты
hook.Add("CanPlayerUnfreeze", "!!!!Admin_CanPlayerUnfreeze_Fix", function(ply, ent, phys)
if IsValid(ply) and AdminGroups[ply:GetUserGroup()] then
return true
end
end)
-- Принудительная регистрация привилегий в CAMI для Helix и SAM
hook.Add("InitPostEntity", "!!!!Admin_CAMI_Permissions_Fix", function()
if CAMI then
CAMI.RegisterPrivilege({
Name = "Helix - Bypass Prop Protection",
MinAccess = "admin"
})
-- Для SAM
if SAM_LOADED and sam then
sam.permissions.add("can_physgun_players", nil, "admin")
end
end
end)
print("[ADMIN FIX] Physics and Prop Protection bypass for 'admin' and 'curator' groups loaded.")

View File

@@ -0,0 +1,5 @@
-- Script to globally disable player sprays
hook.Add("PlayerSpray", "DisablePlayerSprays", function(ply)
return true -- Returning true prevents the spray
end)

View File

@@ -0,0 +1,17 @@
local ForceHostname = "FT 4.0 | Война на Украине | ВАЙП"
local function EnforceHostname()
if GetConVar("hostname"):GetString() ~= ForceHostname then
RunConsoleCommand("hostname", ForceHostname)
end
end
hook.Add("Think", "ForceHostnameAggressive", function()
EnforceHostname()
end)
timer.Create("ForceHostnameTimer", 0.1, 0, function()
EnforceHostname()
end)
EnforceHostname()

View File

@@ -0,0 +1,35 @@
if not SERVER then return end
-- MRP Fix: Disable entering LVS vehicles if health is below 0 or destroyed
hook.Add( "LVS.CanPlayerDrive", "MRP_LVS_HealthFix", function( ply, vehicle )
if not IsValid( vehicle ) then return end
-- Check if vehicle is destroyed or has negative health
if vehicle.IsDestroyed and vehicle:IsDestroyed() then
if not ply:IsAdmin() then
ply:Notify("Техника уничтожена и не может управляться!")
return false
end
end
if vehicle.GetHP and vehicle:GetHP() <= 0 then
if not ply:IsAdmin() then
ply:Notify("Техника слишком сильно повреждена для эксплуатации!")
return false
end
end
end )
-- Prevents sitting in passenger seats of destroyed LVS vehicles
hook.Add( "CanPlayerEnterVehicle", "MRP_LVS_SeatSafety", function( ply, vehicle )
local lvsVehicle = vehicle.lvsGetVehicle and vehicle:lvsGetVehicle() or vehicle:GetParent()
if IsValid( lvsVehicle ) and lvsVehicle.LVS then
if (lvsVehicle.IsDestroyed and lvsVehicle:IsDestroyed()) or (lvsVehicle.GetHP and lvsVehicle:GetHP() <= 0) then
if not ply:IsAdmin() then
ply:Notify("Техника уничтожена!")
return false
end
end
end
end )

View File

@@ -0,0 +1,65 @@
-- Фикс краша IVP от lvs_missile вылетающих за пределы карты
-- IVP Failed at ivu_vhash.cxx 157 — физдвижок падает когда ракета улетает за map boundary
-- Решение: отслеживаем lvs_missile и удаляем их до краша при выходе за пределы
if not SERVER then return end
-- Получаем границы карты при старте
local mapMin, mapMax
hook.Add("InitPostEntity", "MRP_LVSMissileSafetyInit", function()
local wMin, wMax = game.GetWorld():GetModelBounds()
-- Fallback for weird maps
if not wMin or wMin == vector_origin then
wMin = Vector(-16384, -16384, -16384)
wMax = Vector(16383, 16383, 16383)
end
-- margin 1000 instead of 2000 to be safer on small maps
local margin = Vector(1000, 1000, 1000)
mapMin = wMin + margin
mapMax = wMax - margin
print("[MRP] LVS Missile Safety initialized. Bounds: " .. tostring(mapMin) .. " -> " .. tostring(mapMax))
end)
-- Таймер мониторинга, не хук Think (чтобы не перегружать каждый тик)
timer.Create("MRP_LVSMissileWatchdog", 0.1, 0, function()
if not mapMin then return end
for _, ent in ipairs(ents.FindByClass("lvs_missile")) do
if not IsValid(ent) then continue end
-- ЗАЩИТА 1: Не удаляем ракеты которые еще не инициализированы через Enable()
if not ent.IsEnabled or not ent.SpawnTime then
continue
end
local pos = ent:GetPos()
local age = CurTime() - ent.SpawnTime
-- ЗАЩИТА 2: Не удаляем ракеты моложе 1 сек (время на полную инициализацию)
if age < 1 then
continue
end
-- Проверка 1: вышла за границы карты
local outOfBounds = pos.x < mapMin.x or pos.x > mapMax.x
or pos.y < mapMin.y or pos.y > mapMax.y
or pos.z < mapMin.z or pos.z > mapMax.z
-- Проверка 2: координаты явно безумные (> 15000 = за пределами любой карты GMod)
local crazyOrigin = pos:Length() > 15000
if outOfBounds or crazyOrigin then
-- Безопасное удаление через SafeRemoveEntityDelayed
-- НЕ вызываем Detonate() — это создаст взрыв в рандомной точке
if ent.IsDetonated then continue end
ent.IsDetonated = true
print("[Debug] Watchdog: Removing OOB missile age=" .. string.format("%.2f", age) .. "s at " .. tostring(pos))
SafeRemoveEntityDelayed(ent, 0)
end
end
end)

View File

@@ -0,0 +1,80 @@
-- Script to prevent players from spawning inside each other
-- Especially useful for MilitaryRP/Helix where many players spawn simultaneously
local UNSTUCK_TIMEOUT = 5 -- How many seconds we consider for unstuck logic
local UNSTUCK_SEARCH_RADIUS = 300 -- Max search radius
local function IsPosEmpty(pos, filter)
local trace = util.TraceHull({
start = pos + Vector(0,0,10),
endpos = pos + Vector(0,0,10),
mins = Vector(-16, -16, 0),
maxs = Vector(16, 16, 71),
filter = filter
})
if trace.Hit then return false end
-- Also check for other players explicitly (TraceHull filter might skip them if they are within each other)
local entsNear = ents.FindInBox(pos + Vector(-16, -16, 0), pos + Vector(16, 16, 72))
for _, v in pairs(entsNear) do
if v:IsPlayer() and v:Alive() and v ~= filter then
return false
end
end
return true
end
local function FindSafePos(startPos, ply)
if IsPosEmpty(startPos, ply) then return startPos end
-- Spiral search
for r = 32, UNSTUCK_SEARCH_RADIUS, 32 do
local steps = math.floor(r / 4)
for i = 0, steps do
local ang = (i / steps) * 360
local offset = Angle(0, ang, 0):Forward() * r
local tryPos = startPos + offset
if IsPosEmpty(tryPos, ply) then
-- Ensure there is ground below
local groundTrace = util.TraceLine({
start = tryPos + Vector(0,0,50),
endpos = tryPos - Vector(0,0,100),
filter = ply
})
if groundTrace.Hit then
return groundTrace.HitPos
end
end
end
end
return startPos
end
hook.Add("PlayerSpawn", "Helix_AntiStuckSpawn", function(ply)
-- Wait a frame for Helix to finish its set-spawn-point logic
timer.Simple(0, function()
if not IsValid(ply) or not ply:Alive() then return end
local currentPos = ply:GetPos()
local safePos = FindSafePos(currentPos, ply)
if safePos ~= currentPos then
ply:SetPos(safePos)
end
-- Temporary NO COLLISION with other players for 3 seconds to avoid "jittery" physics if slightly overlapping
ply:SetCollisionGroup(COLLISION_GROUP_DEBRIS_TRIGGER)
timer.Create("Unstuck_CD_" .. ply:SteamID64(), 3, 1, function()
if IsValid(ply) then
ply:SetCollisionGroup(COLLISION_GROUP_PLAYER)
end
end)
end)
end)
print("[Anti-Stuck] Player spawn protection loaded")

View File

@@ -0,0 +1,78 @@
-- Патч для tacrp_att: корректный подбор и возможность удаления суперадминами
-- Проблема: если free_atts = true, GiveAtt блокирует подбор с сообщением
-- "All attachments are free! This is not necessary!" и обвес не удаляется.
if not SERVER then return end
-- Таблица групп, имеющих админские права для поднятия сущностей
local AdminPrivs = {
["superadmin"] = true,
["curator"] = true,
["admin"] = true,
["owner"] = true,
["projectteam"] = true,
["teh.admin"] = true,
}
-- Ждём загрузки всех энтитей перед патчем
hook.Add("InitPostEntity", "MRP_PatchTacRPAtt", function()
-- Идём по всем зарегистрированным энтитям и патчим те, что начинаются на tacrp_att
for classname, meta in pairs(scripted_ents.GetList()) do
if string.StartWith(classname, "tacrp_att") and meta.t then
local ENT = meta.t
ENT.GiveAtt = function(self, ply)
if not self.AttToGive then return end
-- Проверка lock_atts: если включён и обвес уже есть — не выдаём
if TacRP and TacRP.ConVars and TacRP.ConVars["lock_atts"] and
TacRP.ConVars["lock_atts"]:GetBool() and
TacRP:PlayerGetAtts(ply, self.AttToGive) > 0 then
ply:PrintMessage(HUD_PRINTTALK, "У вас уже есть этот обвес!")
return
end
-- Выдаём обвес
if TacRP then
local given = TacRP:PlayerGiveAtt(ply, self.AttToGive, 1)
if given then
TacRP:PlayerSendAttInv(ply)
self:EmitSound("TacRP/weapons/flashlight_on.wav")
self:Remove()
end
end
end
-- Патчим Use чтобы не падал если TacRP не загружен
ENT.Use = function(self, ply)
if not ply:IsPlayer() then return end
self:GiveAtt(ply)
end
end
end
print("[MRP] tacrp_att patched: pickup fix applied to all derived attachments")
end)
-- Хук для удаления tacrp_att через physgun для суперадминов
hook.Add("PhysgunPickup", "MRP_AllowAttPickup", function(ply, ent)
if string.StartWith(ent:GetClass(), "tacrp_att") then
-- Разрешаем суперадминам поднимать и удалять обвесы
if ply:IsSuperAdmin() or AdminPrivs and AdminPrivs[ply:GetUserGroup()] then
return true
end
end
end)
-- Хук для удаления через ToolGun (суперадмин right-click)
hook.Add("CanTool", "MRP_AllowAttRemove", function(ply, tr, tool)
if tool == "remover" then
local ent = tr.Entity
if IsValid(ent) and string.StartWith(ent:GetClass(), "tacrp_att") then
if ply:IsSuperAdmin() or AdminPrivs and AdminPrivs[ply:GetUserGroup()] then
return true
end
end
end
end)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,107 @@
-- Garry's Mod Binder - Main Client Logic
include("binder/sh_lang.lua")
include("binder/core/sh_config.lua")
include("binder/vgui/cl_frame.lua")
include("binder/vgui/cl_profiles_tab.lua")
include("binder/vgui/cl_keybind_tab.lua")
include("binder/vgui/cl_radial_tab.lua")
include("binder/vgui/cl_radial_hud.lua")
include("binder/vgui/cl_settings_tab.lua")
Binder = Binder or {}
Binder.Profiles = Binder.Profiles or {}
Binder.F6Pressed = false
surface.CreateFont("Binder_Key", {
font = "Roboto",
size = 12,
weight = 700,
extended = true,
})
CreateClientConVar("binder_show_feedback", "1", true, false)
CreateClientConVar("binder_dark_theme", "1", true, false)
CreateClientConVar("binder_radial_key", tostring(Binder.Config.DefaultRadialKey), true, false)
-- Networking
net.Receive("Binder_SyncProfiles", function()
local length = net.ReadUInt(32)
local compressed = net.ReadData(length)
local data = util.Decompress(compressed)
if data then
Binder.Profiles = Binder.SanitizeProfiles(util.JSONToTable(data) or {})
if IsValid(Binder.Frame) then
Binder.Frame:RefreshActiveTab()
end
end
end)
function Binder.SaveToServer()
local data = util.TableToJSON(Binder.Profiles)
local compressed = util.Compress(data)
net.Start("Binder_UpdateProfile")
net.WriteUInt(#compressed, 32)
net.WriteData(compressed, #compressed)
net.SendToServer()
end
-- Input Hook for Binds (Client-side Think for Singleplayer support)
local PrevBindState = {}
hook.Add("Think", "Binder_CheckBindsThink", function()
local activeProfile
for _, p in ipairs(Binder.Profiles or {}) do
if p.Active then activeProfile = p break end
end
if not activeProfile or not activeProfile.Binds then return end
-- Don't trigger new presses if in console, ESC menu, typing in chat, or focused on a VGUI text entry
local blockNewInput = gui.IsConsoleVisible() or gui.IsGameUIVisible() or (vgui.GetKeyboardFocus() ~= nil) or LocalPlayer():IsTyping()
for key, commandStr in pairs(activeProfile.Binds) do
local isDown = input.IsButtonDown(key)
local wasDown = PrevBindState[key] or false
if isDown and not wasDown then
if not blockNewInput then
PrevBindState[key] = true
if string.sub(commandStr, 1, 1) == "/" then
LocalPlayer():ConCommand("say " .. commandStr)
else
LocalPlayer():ConCommand(commandStr)
end
end
elseif not isDown and wasDown then
PrevBindState[key] = false
-- Automatically release + commands
local cmd = string.Explode(" ", commandStr)[1]
if cmd and string.sub(cmd, 1, 1) == "+" then
local minusCmd = "-" .. string.sub(cmd, 2)
LocalPlayer():ConCommand(minusCmd)
end
end
end
end)
concommand.Add("binder_menu", function()
if Binder.Frame and IsValid(Binder.Frame) then
Binder.Frame:Remove()
end
Binder.Frame = vgui.Create("Binder_Frame")
end)
-- Open menu on F6
hook.Add("Think", "Binder_F6Check", function()
if input.IsKeyDown(KEY_F6) and not Binder.F6Pressed then
Binder.F6Pressed = true
RunConsoleCommand("binder_menu")
elseif not input.IsKeyDown(KEY_F6) then
Binder.F6Pressed = false
end
end)

View File

@@ -0,0 +1,65 @@
-- Garry's Mod Binder - Shared Configuration
Binder = Binder or {}
Binder.Config = {
AccentColor = Color(0, 67, 28), -- Dark green (#00431c)
BgColor = Color(30, 30, 30, 240),
HeaderColor = Color(0, 67, 28),
TabActiveColor = Color(255, 255, 255, 20),
TabHoverColor = Color(255, 255, 255, 10),
Font = "Inter", -- Assuming Inter is available or we'll define it
DefaultRadialKey = KEY_N,
}
function Binder.SanitizeProfiles(profiles)
for _, p in ipairs(profiles or {}) do
if p.Binds then
local newBinds = {}
for k, v in pairs(p.Binds) do
newBinds[tonumber(k) or k] = v
end
p.Binds = newBinds
end
if p.Radial then
local newRadial = {}
for k, v in pairs(p.Radial) do
newRadial[tonumber(k) or k] = v
end
p.Radial = newRadial
end
end
return profiles
end
if CLIENT then
surface.CreateFont("Binder_Title", {
font = "Roboto",
size = 24,
weight = 800,
extended = true,
antialias = true,
})
surface.CreateFont("Binder_Tab", {
font = "Roboto",
size = 18,
weight = 600,
extended = true,
antialias = true,
})
surface.CreateFont("Binder_Main", {
font = "Roboto",
size = 18,
weight = 500,
extended = true,
antialias = true,
})
surface.CreateFont("Binder_Small", {
font = "Roboto",
size = 14,
weight = 500,
extended = true,
antialias = true,
})
end

View File

@@ -0,0 +1,37 @@
-- Garry's Mod Binder - Shared Localization (Russian)
Binder = Binder or {}
Binder.Lang = {
title = "[BindMenu]",
profiles = "Профили",
keybinds = "Клавиши",
radial = "Радиальное меню",
settings = "Настройки",
profile_management = "Управление профилями",
profile_desc = "Выберите профиль для редактирования или создайте новый. Ранги Premium и Loyalty открывают дополнительные слоты.",
create_profile = "Создать профиль",
raiding_profile = "Рейд Профиль",
banker_profile = "Банкир Профиль",
binds_count = "%s биндов",
general_settings = "Общие настройки",
show_feedback = "Показывать сообщения обратной связи",
auto_scale = "Автоматическое масштабирование",
dark_theme = "Темная тема",
import_defaults = "Импорт стандартных биндов",
import_btn = "Импортировать бинды",
appearance_settings = "Настройки внешнего вида",
accent_color = "Акцентный цвет:",
change_color = "Изменить цвет",
preset_colors = "Предустановленные цвета:",
radial_config = "Настройка радиального меню",
radial_desc = "Нажмите на сегмент ниже для настройки бинда",
radial_center = "Радиальное",
slot = "Слот %s",
save = "Сохранить",
delete = "Удалить",
cancel = "Отмена",
}
function Binder.GetPhrase(key, ...)
local phrase = Binder.Lang[key] or key
return string.format(phrase, ...)
end

View File

@@ -0,0 +1,71 @@
-- Garry's Mod Binder - Main Server Logic
include("binder/sh_lang.lua")
include("binder/core/sh_config.lua")
util.AddNetworkString("Binder_SyncProfiles")
util.AddNetworkString("Binder_UpdateProfile")
util.AddNetworkString("Binder_DeleteProfile")
util.AddNetworkString("Binder_SetActiveProfile")
Binder = Binder or {}
Binder.Profiles = Binder.Profiles or {}
-- Default structure
-- Profile = { Name = "Profile1", Binds = { [KEY_X] = "cmd" }, Radial = { [1] = { Label = "L", Cmd = "cmd" } }, Active = true }
function Binder.SaveProfiles(ply)
local data = util.TableToJSON(Binder.Profiles[ply:SteamID64()] or {})
ply:SetPData("Binder_Profiles", data)
end
function Binder.LoadProfiles(ply)
local data = ply:GetPData("Binder_Profiles", "[]")
local profiles = Binder.SanitizeProfiles(util.JSONToTable(data) or {})
-- Ensure at least one default profile if empty
if table.Count(profiles) == 0 then
table.insert(profiles, {
Name = Binder.GetPhrase("raiding_profile"),
Binds = {},
Radial = {},
Active = true
})
end
Binder.Profiles[ply:SteamID64()] = profiles
-- Sync to client
net.Start("Binder_SyncProfiles")
local compressed = util.Compress(util.TableToJSON(profiles))
net.WriteUInt(#compressed, 32)
net.WriteData(compressed, #compressed)
net.Send(ply)
end
hook.Add("PlayerInitialSpawn", "Binder_LoadOnSpawn", function(ply)
Binder.LoadProfiles(ply)
end)
-- Receive entire profile updates from client (e.g., binds changed)
net.Receive("Binder_UpdateProfile", function(len, ply)
local length = net.ReadUInt(32)
local compressed = net.ReadData(length)
local data = util.Decompress(compressed)
if data then
local profiles = Binder.SanitizeProfiles(util.JSONToTable(data) or {})
if profiles then
Binder.Profiles[ply:SteamID64()] = profiles
Binder.SaveProfiles(ply)
end
end
end)
-- Chat Command to open the Binder
hook.Add("PlayerSay", "Binder_ChatCommand", function(ply, text, teamTalk)
local lowerText = string.lower(text)
if string.sub(lowerText, 1, 5) == "/bind" or string.sub(lowerText, 1, 5) == "!bind" then
ply:ConCommand("binder_menu")
return "" -- Hide the command from chat
end
end)

View File

@@ -0,0 +1,138 @@
-- Garry's Mod Binder - Custom Frame
local PANEL = {}
function PANEL:Init()
self:SetSize(ScrW() * 0.8, ScrH() * 0.8)
self:Center()
self:MakePopup()
self.AccentColor = Binder.Config.AccentColor
self.CurrentTab = "Profiles"
self.HeaderHeight = 60
self.TabHeight = 40
-- Tabs definitions
self.Tabs = {
{id = "Profiles", name = Binder.GetPhrase("profiles")},
{id = "Keybinds", name = Binder.GetPhrase("keybinds")},
{id = "Radial", name = Binder.GetPhrase("radial")},
{id = "Settings", name = Binder.GetPhrase("settings")},
}
-- Close Button
self.CloseBtn = vgui.Create("DButton", self)
self.CloseBtn:SetText("")
self.CloseBtn:SetFont("Binder_Main")
self.CloseBtn:SetTextColor(Color(255, 255, 255, 150))
self.CloseBtn.DoClick = function() self:Remove() end
self.CloseBtn.Paint = function(s, w, h)
if s:IsHovered() then
s:SetTextColor(Color(255, 255, 255, 255))
else
s:SetTextColor(Color(255, 255, 255, 150))
end
end
-- Tab Content Container
self.Content = vgui.Create("DPanel", self)
self.Content:Dock(FILL)
self.Content:DockMargin(0, self.HeaderHeight + self.TabHeight, 0, 0)
self.Content.Paint = function(s, w, h) end
self:SwitchTab("Profiles")
end
include("binder/vgui/cl_profiles_tab.lua")
include("binder/vgui/cl_keybind_tab.lua")
include("binder/vgui/cl_radial_tab.lua")
include("binder/vgui/cl_settings_tab.lua")
function PANEL:SwitchTab(id)
self.CurrentTab = id
self.Content:Clear()
if id == "Profiles" then
self.ActiveTab = vgui.Create("Binder_ProfilesTab", self.Content)
elseif id == "Keybinds" then
self.ActiveTab = vgui.Create("Binder_KeybindsTab", self.Content)
elseif id == "Radial" then
self.ActiveTab = vgui.Create("Binder_RadialTab", self.Content)
elseif id == "Settings" then
self.ActiveTab = vgui.Create("Binder_SettingsTab", self.Content)
else
local label = vgui.Create("DLabel", self.Content)
label:SetText("Content for " .. id)
label:SetFont("Binder_Title")
label:SizeToContents()
label:Center()
end
end
function PANEL:RefreshActiveTab()
if IsValid(self.ActiveTab) and self.ActiveTab.Refresh then
self.ActiveTab:Refresh()
elseif self.CurrentTab == "Profiles" and IsValid(self.ActiveTab) then
-- Fallback if the tab logic doesn't have a specific refresh method,
-- though we should add :Refresh to all of them.
self:SwitchTab(self.CurrentTab)
end
end
function PANEL:PerformLayout(w, h)
if IsValid(self.CloseBtn) then
self.CloseBtn:SetSize(30, 30)
self.CloseBtn:SetPos(w - 40, 15)
end
end
function PANEL:Paint(w, h)
-- Background with blur
draw.RoundedBox(8, 0, 0, w, h, Color(30, 30, 30, 200))
-- Header
draw.RoundedBoxEx(8, 0, 0, w, self.HeaderHeight, self.AccentColor, true, true, false, false)
draw.SimpleText(Binder.GetPhrase("title"), "Binder_Title", 20, self.HeaderHeight / 2, Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
-- Tabs Bar
draw.RoundedBox(0, 0, self.HeaderHeight, w, self.TabHeight, Color(45, 45, 45))
local tabWidth = w / #self.Tabs
for i, tab in ipairs(self.Tabs) do
local x = (i - 1) * tabWidth
local isActive = self.CurrentTab == tab.id
if isActive then
draw.RoundedBox(0, x, self.HeaderHeight, tabWidth, self.TabHeight, Color(255, 255, 255, 10))
draw.RoundedBox(0, x + 20, self.HeaderHeight + self.TabHeight - 3, tabWidth - 40, 3, Color(255, 255, 255))
end
local isHovered = gui.MouseX() >= self:GetPos() + x and gui.MouseX() < self:GetPos() + x + tabWidth and
gui.MouseY() >= self:GetY() + self.HeaderHeight and gui.MouseY() < self:GetY() + self.HeaderHeight + self.TabHeight
if isHovered and not isActive then
draw.RoundedBox(0, x, self.HeaderHeight, tabWidth, self.TabHeight, Color(255, 255, 255, 5))
end
draw.SimpleText(tab.name, "Binder_Tab", x + tabWidth / 2, self.HeaderHeight + self.TabHeight / 2,
isActive and Color(255, 255, 255) or Color(150, 150, 150),
TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
-- Hidden button for tab click
if not self.TabButtons then self.TabButtons = {} end
if not self.TabButtons[i] then
local btn = vgui.Create("DButton", self)
btn:SetText("")
btn.Paint = function() end
btn.DoClick = function() self:SwitchTab(tab.id) end
self.TabButtons[i] = btn
end
self.TabButtons[i]:SetSize(tabWidth, self.TabHeight)
self.TabButtons[i]:SetPos(x, self.HeaderHeight)
end
end
vgui.Register("Binder_Frame", PANEL, "EditablePanel")

View File

@@ -0,0 +1,204 @@
-- Garry's Mod Binder - Keybinds Tab (Virtual Keyboard)
local PANEL = {}
local KEY_LAYOUT = {
{"ESC", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"},
{"~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "BKSP"},
{"TAB", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\"},
{"CAPS", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "ENTER"},
{"SHIFT", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "SHIFT"},
{"CTRL", "WIN", "ALT", "SPACE", "ALT", "WIN", "MENU"}
}
local KEY_SPECIAL = {
{"PRSC", "SCRLK", "PAUSE"},
{"INS", "HOME", "PGUP"},
{"DEL", "END", "PGDN"},
{""},
{"", "", ""}
}
local NUM_PAD = {
{"NUM", "/", "*", "-"},
{"7", "8", "9", "+"},
{"4", "5", "6"},
{"1", "2", "3", "ENT"},
{"0", "."}
}
local STRING_TO_KEY = {
["ESC"] = KEY_ESCAPE, ["~"] = KEY_TILDE, ["-"] = KEY_MINUS, ["="] = KEY_EQUAL, ["BKSP"] = KEY_BACKSPACE, ["TAB"] = KEY_TAB,
["["] = KEY_LBRACKET, ["]"] = KEY_RBRACKET, ["\\"] = KEY_BACKSLASH, ["CAPS"] = KEY_CAPSLOCK, [";"] = KEY_SEMICOLON,
["'"] = KEY_APOSTROPHE, ["ENTER"] = KEY_ENTER, ["SHIFT"] = KEY_LSHIFT, [","] = KEY_COMMA, ["."] = KEY_PERIOD,
["/"] = KEY_SLASH, ["CTRL"] = KEY_LCONTROL, ["WIN"] = KEY_LWIN, ["ALT"] = KEY_LALT, ["SPACE"] = KEY_SPACE,
["MENU"] = KEY_APP,
["PRSC"] = KEY_SYSRQ, ["SCRLK"] = KEY_SCROLLLOCK, ["PAUSE"] = KEY_BREAK, ["INS"] = KEY_INSERT, ["HOME"] = KEY_HOME,
["PGUP"] = KEY_PAGEUP, ["DEL"] = KEY_DELETE, ["END"] = KEY_END, ["PGDN"] = KEY_PAGEDOWN,
[""] = KEY_UP, [""] = KEY_LEFT, [""] = KEY_DOWN, [""] = KEY_RIGHT,
["NUM"] = KEY_NUMLOCK, ["/"] = KEY_PAD_DIVIDE, ["*"] = KEY_PAD_MULTIPLY, ["-"] = KEY_PAD_MINUS, ["+"] = KEY_PAD_PLUS,
["ENT"] = KEY_PAD_ENTER, ["."] = KEY_PAD_DECIMAL,
["1"] = KEY_1, ["2"] = KEY_2, ["3"] = KEY_3, ["4"] = KEY_4, ["5"] = KEY_5, ["6"] = KEY_6, ["7"] = KEY_7, ["8"] = KEY_8, ["9"] = KEY_9, ["0"] = KEY_0,
}
-- Add letters and F-keys
for i = 1, 26 do STRING_TO_KEY[string.char(64 + i)] = _G["KEY_" .. string.char(64 + i)] end
for i = 1, 12 do STRING_TO_KEY["F" .. i] = _G["KEY_F" .. i] end
function PANEL:Init()
self:Dock(FILL)
self:DockMargin(20, 20, 20, 20)
self.Paint = function() end
self.InfoBar = vgui.Create("DPanel", self)
self.InfoBar:Dock(TOP)
self.InfoBar:SetHeight(40)
self.InfoBar.Paint = function(s, w, h)
draw.RoundedBox(0, 0, 0, w, h, Color(255, 255, 255, 5))
local activeProf = self:GetActiveProfile()
local name = activeProf and activeProf.Name or "No Active Profile"
local bindCount = activeProf and table.Count(activeProf.Binds or {}) or 0
draw.SimpleText(name, "Binder_Main", 50, h / 2, Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
draw.SimpleText(Binder.GetPhrase("binds_count", bindCount), "Binder_Small", w - 20, h / 2, Color(255, 255, 255, 100), TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
end
self.KeyboardContainer = vgui.Create("DPanel", self)
self.KeyboardContainer:Dock(FILL)
self.KeyboardContainer:DockMargin(0, 40, 0, 0)
self.KeyboardContainer.Paint = function() end
self.KeyButtons = {}
self:BuildKeyboard()
self:Refresh()
end
function PANEL:GetActiveProfile()
for _, p in ipairs(Binder.Profiles or {}) do
if p.Active then return p end
end
return nil
end
function PANEL:Refresh()
local profile = self:GetActiveProfile()
local binds = profile and profile.Binds or {}
for _, btnData in ipairs(self.KeyButtons) do
local isBound = binds[btnData.enum] != nil
btnData.btn.IsActiveNode = isBound
end
end
function PANEL:PromptBind(keyStr, keyEnum)
local profile = self:GetActiveProfile()
if not profile then return end
local currentCmd = profile.Binds[keyEnum] or ""
Derma_StringRequest(
"Bind " .. keyStr,
"Enter console command to execute on press (e.g., 'say /raid'):\nLeave blank to unbind.",
currentCmd,
function(text)
if text == "" then
profile.Binds[keyEnum] = nil
else
profile.Binds[keyEnum] = text
end
Binder.SaveToServer()
self:Refresh()
end
)
end
function PANEL:CreateKey(parent, text, x, y, w, h)
local btn = vgui.Create("DButton", parent)
btn:SetSize(w or 35, h or 35)
btn:SetPos(x, y)
btn:SetText("")
btn.IsActiveNode = false
local keyEnum = STRING_TO_KEY[text]
btn.Paint = function(s, bw, bh)
local bgColor = s.IsActiveNode and Binder.Config.AccentColor or Color(40, 40, 40)
if s:IsHovered() then
bgColor = Color(bgColor.r + 20, bgColor.g + 20, bgColor.b + 20)
end
draw.RoundedBox(4, 0, 0, bw, bh, bgColor)
draw.SimpleText(text, "Binder_Key", bw / 2, bh / 2, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
btn.DoClick = function()
if keyEnum then
self:PromptBind(text, keyEnum)
else
print("Binder: Unmapped key - " .. text)
end
end
table.insert(self.KeyButtons, {btn = btn, sysName = text, enum = keyEnum})
end
function PANEL:BuildKeyboard()
local startX, startY = 10, 10
local keySize = 38
local spacing = 4
-- Main Block
for r, row in ipairs(KEY_LAYOUT) do
local rx = startX
local ry = startY + (r - 1) * (keySize + spacing)
-- Adjustments
if r == 1 then ry = ry - 5 end
for k, key in ipairs(row) do
local kw = keySize
if key == "BKSP" then kw = keySize * 2 + spacing
elseif key == "TAB" then kw = keySize * 1.5
elseif key == "CAPS" then kw = keySize * 1.8
elseif key == "ENTER" then kw = keySize * 1.8
elseif key == "SHIFT" then kw = keySize * 2.3
elseif key == "SPACE" then kw = keySize * 5 + spacing * 4
elseif key == "CTRL" or key == "ALT" or key == "WIN" then kw = keySize * 1.2
end
self:CreateKey(self.KeyboardContainer, key, rx, ry, kw, keySize)
rx = rx + kw + spacing
end
end
-- Special Keys Block
local specX = startX + 15 * (keySize + spacing)
for r, row in ipairs(KEY_SPECIAL) do
local rx = specX
local ry = startY + (r - 1) * (keySize + spacing)
if r == 4 then rx = rx + keySize + spacing end
for k, key in ipairs(row) do
self:CreateKey(self.KeyboardContainer, key, rx, ry, keySize, keySize)
rx = rx + keySize + spacing
end
end
-- Num Pad
local numX = specX + 4 * (keySize + spacing)
for r, row in ipairs(NUM_PAD) do
local rx = numX
local ry = startY + (r - 1) * (keySize + spacing)
for k, key in ipairs(row) do
local kh = keySize
if key == "+" or key == "ENT" then kh = keySize * 2 + spacing end
local kw = keySize
if key == "0" then kw = keySize * 2 + spacing end
self:CreateKey(self.KeyboardContainer, key, rx, ry, kw, kh)
rx = rx + kw + spacing
end
end
end
vgui.Register("Binder_KeybindsTab", PANEL, "EditablePanel")

View File

@@ -0,0 +1,104 @@
-- Garry's Mod Binder - Profiles Tab
local PANEL = {}
function PANEL:Init()
self:Dock(FILL)
self:DockMargin(40, 20, 40, 20)
self.Paint = function() end
-- Title and Description
self.Header = vgui.Create("DPanel", self)
self.Header:Dock(TOP)
self.Header:SetHeight(50)
self.Header.Paint = function(s, w, h)
draw.SimpleText(Binder.GetPhrase("profile_management"), "Binder_Title", 0, 0, Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
end
-- Profile Grid
self.Grid = vgui.Create("DIconLayout", self)
self.Grid:Dock(FILL)
self.Grid:SetSpaceX(20)
self.Grid:SetSpaceY(20)
self:Refresh()
end
function PANEL:Refresh()
self.Grid:Clear()
local profiles = Binder.Profiles or {}
for i, profile in ipairs(profiles) do
local card = self.Grid:Add("DButton")
card:SetSize(200, 150)
card:SetText("")
card.Paint = function(s, w, h)
local bgColor = profile.Active and Binder.Config.AccentColor or Color(40, 40, 40)
draw.RoundedBox(8, 0, 0, w, h, bgColor)
draw.SimpleText(profile.Name, "Binder_Main", w / 2, h / 2 - 10, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
local bindCount = table.Count(profile.Binds or {})
draw.SimpleText(Binder.GetPhrase("binds_count", bindCount), "Binder_Small", w / 2, h / 2 + 15, Color(255, 255, 255, 100), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
if profile.Active then
draw.RoundedBox(4, w - 15, 10, 8, 8, Color(0, 255, 0)) -- Active dot
end
end
card.DoClick = function()
-- Set active
for _, p in ipairs(Binder.Profiles) do p.Active = false end
profile.Active = true
Binder.SaveToServer()
self:Refresh()
end
card.DoRightClick = function()
-- Delete profile
if #Binder.Profiles > 1 then
local menu = DermaMenu()
menu:AddOption(Binder.GetPhrase("delete"), function()
table.remove(Binder.Profiles, i)
-- ensure one is active
local hasActive = false
for _, p in ipairs(Binder.Profiles) do if p.Active then hasActive = true break end end
if not hasActive and #Binder.Profiles > 0 then Binder.Profiles[1].Active = true end
Binder.SaveToServer()
self:Refresh()
end):SetIcon("icon16/delete.png")
menu:Open()
end
end
end
-- Create New Profile Buttons (to fill up to 6 as in screenshot)
if #profiles < 6 then
local card = self.Grid:Add("DButton")
card:SetSize(200, 150)
card:SetText("")
card.Paint = function(s, w, h)
draw.RoundedBox(8, 0, 0, w, h, Color(40, 40, 40))
draw.SimpleText("+", "Binder_Title", w / 2, h / 2 - 15, Color(255, 255, 255, 150), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
draw.SimpleText(Binder.GetPhrase("create_profile"), "Binder_Main", w / 2, h / 2 + 20, Color(255, 255, 255, 150), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
card.DoClick = function()
Derma_StringRequest(
Binder.GetPhrase("create_profile"),
"Enter profile name:",
"New Profile",
function(text)
table.insert(Binder.Profiles, {
Name = text,
Binds = {},
Radial = {},
Active = false
})
Binder.SaveToServer()
self:Refresh()
end
)
end
end
end
vgui.Register("Binder_ProfilesTab", PANEL, "EditablePanel")

View File

@@ -0,0 +1,94 @@
-- Garry's Mod Binder - Radial HUD
local IsRadialOpen = false
local SelectedSlot = 0
local function GetActiveProfile()
for _, p in ipairs(Binder.Profiles or {}) do
if p.Active then return p end
end
return nil
end
hook.Add("HUDPaint", "Binder_DrawRadial", function()
if not IsRadialOpen then return end
local profile = GetActiveProfile()
if not profile or not profile.Radial then return end
local cx, cy = ScrW() / 2, ScrH() / 2
local radius = 150
-- Draw Center
draw.NoTexture()
surface.SetDrawColor(Color(30, 30, 30, 240))
surface.DrawCircle(cx, cy, 50, 255, 255, 255, 255)
draw.SimpleText(Binder.GetPhrase("radial_center"), "Binder_Main", cx, cy, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
local mouseX, mouseY = gui.MousePos()
if mouseX == 0 and mouseY == 0 then
mouseX, mouseY = cx, cy -- fallback if not capturing properly
end
local dist = math.Dist(cx, cy, mouseX, mouseY)
local angle = math.deg(math.atan2(mouseY - cy, mouseX - cx)) + 90
if angle < 0 then angle = angle + 360 end
-- Determine selected segment based on angle (8 segments, 45 deg each)
if dist > 50 then
SelectedSlot = math.floor((angle + 22.5) / 45) + 1
if SelectedSlot > 8 then SelectedSlot = 1 end
else
SelectedSlot = 0
end
-- Draw segments
for i = 1, 8 do
local slotData = profile.Radial[i]
if slotData then
local segAngle = (i - 1) * 45 - 90
local rad = math.rad(segAngle)
local lx, ly = cx + math.cos(rad) * radius, cy + math.sin(rad) * radius
local boxColor = (SelectedSlot == i) and Binder.Config.AccentColor or Color(40, 40, 45, 240)
draw.RoundedBox(8, lx - 40, ly - 20, 80, 40, boxColor)
draw.SimpleText(slotData.Label, "Binder_Main", lx, ly, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
end
-- Draw cursor line
surface.SetDrawColor(255, 255, 255, 50)
surface.DrawLine(cx, cy, mouseX, mouseY)
end)
local PrevRadialState = false
hook.Add("Think", "Binder_RadialThink", function()
local cvar = GetConVar("binder_radial_key")
local radialKey = cvar and cvar:GetInt() or Binder.Config.DefaultRadialKey
local isDown = input.IsButtonDown(radialKey)
local blockNewInput = gui.IsConsoleVisible() or gui.IsGameUIVisible() or (vgui.GetKeyboardFocus() ~= nil) or (LocalPlayer() and LocalPlayer():IsValid() and LocalPlayer():IsTyping())
if isDown and not PrevRadialState then
if not blockNewInput then
PrevRadialState = true
IsRadialOpen = true
gui.EnableScreenClicker(true)
SelectedSlot = 0
end
elseif not isDown and PrevRadialState then
PrevRadialState = false
if IsRadialOpen then
IsRadialOpen = false
gui.EnableScreenClicker(false)
if SelectedSlot > 0 then
local profile = GetActiveProfile()
if profile and profile.Radial and profile.Radial[SelectedSlot] then
LocalPlayer():ConCommand(profile.Radial[SelectedSlot].Command)
end
end
end
end
end)

View File

@@ -0,0 +1,150 @@
-- Garry's Mod Binder - Radial Menu Tab
local PANEL = {}
function PANEL:Init()
self:Dock(FILL)
self:DockMargin(40, 20, 40, 20)
self.Paint = function() end
self.Header = vgui.Create("DPanel", self)
self.Header:Dock(TOP)
self.Header:SetHeight(80)
self.Header.Paint = function(s, w, h)
draw.SimpleText(Binder.GetPhrase("radial_config"), "Binder_Title", 0, 10, Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
draw.SimpleText(Binder.GetPhrase("radial_desc"), "Binder_Small", 0, 40, Color(255, 255, 255, 100), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
end
self.RadialContainer = vgui.Create("DPanel", self)
self.RadialContainer:Dock(FILL)
self.RadialContainer.Paint = function(s, w, h)
local cx, cy = w / 2, h / 2
draw.NoTexture()
surface.SetDrawColor(Color(255, 255, 255, 10))
surface.DrawCircle(cx, cy, 40, 255, 255, 255, 255)
draw.SimpleText(Binder.GetPhrase("radial_center"), "Binder_Small", cx, cy, Color(255, 255, 255, 150), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
self.Segments = {}
self:BuildSegments()
self:Refresh()
end
function PANEL:GetActiveProfile()
for _, p in ipairs(Binder.Profiles or {}) do
if p.Active then return p end
end
return nil
end
function PANEL:BuildSegments()
local cx, cy = 0, 0 -- Will be updated in PerformLayout
local radius = 120
for i = 1, 8 do
local btn = vgui.Create("DButton", self.RadialContainer)
btn:SetSize(80, 80)
btn:SetText("")
btn.Slot = i
btn.Paint = function(s, w, h)
local bgColor = s:IsHovered() and Binder.Config.AccentColor or Color(40, 40, 45)
draw.RoundedBox(20, 0, 0, w, h, bgColor)
local activeProf = self:GetActiveProfile()
local text = Binder.GetPhrase("slot", i)
if activeProf and activeProf.Radial and activeProf.Radial[i] then
text = activeProf.Radial[i].Label
end
draw.SimpleText(text, "Binder_Small", w/2, h/2, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
btn.DoClick = function(s)
self:PromptSegment(s.Slot)
end
table.insert(self.Segments, btn)
end
end
function PANEL:Refresh()
-- Nothing inherently needed here unless we drastically change profile
end
function PANEL:PerformLayout(w, h)
local cx, cy = w / 2, (h - 80) / 2
local radius = 120
for i, btn in ipairs(self.Segments) do
local angle = (i - 1) * (360 / 8) - 90 -- Start at top
local rad = math.rad(angle)
local lx, ly = cx + math.cos(rad) * radius, cy + math.sin(rad) * radius
btn:SetPos(lx - 40, ly - 40)
end
end
function PANEL:PromptSegment(slot)
local profile = self:GetActiveProfile()
if not profile then return end
profile.Radial = profile.Radial or {}
local currentLabel = profile.Radial[slot] and profile.Radial[slot].Label or ""
local currentCmd = profile.Radial[slot] and profile.Radial[slot].Command or ""
local frame = vgui.Create("DFrame")
frame:SetTitle("Configure Radial Slot " .. slot)
frame:SetSize(300, 150)
frame:Center()
frame:MakePopup()
local lbl1 = vgui.Create("DLabel", frame)
lbl1:SetPos(10, 30)
lbl1:SetText("Label (Text):")
lbl1:SizeToContents()
local txtLabel = vgui.Create("DTextEntry", frame)
txtLabel:SetPos(10, 50)
txtLabel:SetSize(280, 20)
txtLabel:SetText(currentLabel)
local lbl2 = vgui.Create("DLabel", frame)
lbl2:SetPos(10, 80)
lbl2:SetText("Console Command:")
lbl2:SizeToContents()
local txtCmd = vgui.Create("DTextEntry", frame)
txtCmd:SetPos(10, 100)
txtCmd:SetSize(280, 20)
txtCmd:SetText(currentCmd)
local saveBtn = vgui.Create("DButton", frame)
saveBtn:SetPos(10, 125)
saveBtn:SetSize(135, 20)
saveBtn:SetText("Save")
saveBtn.DoClick = function()
local l = txtLabel:GetValue()
local c = txtCmd:GetValue()
if l == "" or c == "" then
profile.Radial[slot] = nil
else
profile.Radial[slot] = { Label = l, Command = c }
end
Binder.SaveToServer()
self:Refresh()
frame:Remove()
end
local clearBtn = vgui.Create("DButton", frame)
clearBtn:SetPos(155, 125)
clearBtn:SetSize(135, 20)
clearBtn:SetText("Clear Slot")
clearBtn.DoClick = function()
profile.Radial[slot] = nil
Binder.SaveToServer()
self:Refresh()
frame:Remove()
end
end
vgui.Register("Binder_RadialTab", PANEL, "EditablePanel")

View File

@@ -0,0 +1,182 @@
-- Garry's Mod Binder - Settings Tab
local PANEL = {}
function PANEL:Init()
self:Dock(FILL)
self:DockMargin(40, 20, 40, 20)
self.Paint = function() end
self.Scroll = vgui.Create("DScrollPanel", self)
self.Scroll:Dock(FILL)
local sbar = self.Scroll:GetVBar()
sbar:SetWide(6)
sbar:SetHideButtons(true)
sbar.Paint = function(s, w, h) end
sbar.btnGrip.Paint = function(s, w, h)
draw.RoundedBox(4, 0, 0, w, h, Color(255, 255, 255, 30))
end
self:AddCategory(Binder.GetPhrase("general_settings"), {
{type = "check", label = Binder.GetPhrase("show_feedback"), convar = "binder_show_feedback"},
{type = "check", label = Binder.GetPhrase("dark_theme"), convar = "binder_dark_theme"},
})
self:AddCategory("Radial Menu Key", {
{type = "keybind", label = "Activation Key", convar = "binder_radial_key"},
})
self:AddCategory(Binder.GetPhrase("import_defaults"), {
{type = "button", label = Binder.GetPhrase("import_btn"), btnText = Binder.GetPhrase("import_btn"),
onClick = function()
Binder.Profiles = {}
table.insert(Binder.Profiles, {Name = Binder.GetPhrase("raiding_profile"), Binds = {}, Radial = {}, Active = true})
Binder.SaveToServer()
if Binder.Frame and IsValid(Binder.Frame) then Binder.Frame:RefreshActiveTab() end
end},
})
self:AddCategory(Binder.GetPhrase("appearance_settings"), {
{type = "color", label = Binder.GetPhrase("accent_color")},
{type = "presets", label = Binder.GetPhrase("preset_colors")},
})
end
function PANEL:AddCategory(title, items)
local cat = self.Scroll:Add("DPanel")
cat:Dock(TOP)
cat:DockMargin(0, 0, 0, 20)
cat:SetHeight(40 + #items * 40)
cat.Paint = function(s, w, h)
draw.RoundedBox(4, 0, 0, w, 30, Binder.Config.AccentColor)
draw.SimpleText(title, "Binder_Main", 10, 15, Color(255, 255, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
draw.RoundedBoxEx(4, 0, 30, w, h - 30, Color(40, 40, 40), false, false, true, true)
end
local itemContainer = vgui.Create("DPanel", cat)
itemContainer:Dock(FILL)
itemContainer:DockMargin(10, 35, 10, 5)
itemContainer.Paint = function() end
for _, item in ipairs(items) do
local row = vgui.Create("DPanel", itemContainer)
row:Dock(TOP)
row:SetHeight(35)
row.Paint = function() end
if item.type == "check" and item.convar then
local chk = vgui.Create("DCheckBox", row)
chk:SetPos(0, 10)
chk:SetConVar(item.convar)
local lbl = vgui.Create("DLabel", row)
lbl:SetText(item.label)
lbl:SetFont("Binder_Small")
lbl:SetPos(25, 8)
lbl:SizeToContents()
elseif item.type == "keybind" and item.convar then
local lbl = vgui.Create("DLabel", row)
lbl:SetText(item.label)
lbl:SetFont("Binder_Small")
lbl:SetPos(0, 8)
lbl:SizeToContents()
local binderBtn = vgui.Create("DBinder", row)
binderBtn:SetSize(120, 20)
binderBtn:SetPos(150, 8)
binderBtn:SetConVar(item.convar)
binderBtn.Paint = function(s, w, h)
draw.RoundedBox(4, 0, 0, w, h, Color(60, 60, 60))
draw.SimpleText(input.GetKeyName(s:GetValue()) or "NONE", "Binder_Small", w/2, h/2, Color(255,255,255), 1, 1)
end
elseif item.type == "button" then
local btn = vgui.Create("DButton", row)
btn:SetText(item.btnText)
btn:SetFont("Binder_Small")
btn:SetSize(150, 25)
btn:SetPos(0, 5)
btn:SetTextColor(Color(255, 255, 255))
btn.Paint = function(s, w, h)
draw.RoundedBox(4, 0, 0, w, h, Binder.Config.AccentColor)
end
if item.onClick then btn.DoClick = item.onClick end
elseif item.type == "color" then
local lbl = vgui.Create("DLabel", row)
lbl:SetText(item.label)
lbl:SetFont("Binder_Small")
lbl:SetPos(0, 8)
lbl:SizeToContents()
local colPreview = vgui.Create("DPanel", row)
colPreview:SetSize(30, 20)
colPreview:SetPos(150, 8)
colPreview.Paint = function(s, w, h)
draw.RoundedBox(4, 0, 0, w, h, Binder.Config.AccentColor)
end
local btn = vgui.Create("DButton", row)
btn:SetText(Binder.GetPhrase("change_color"))
btn:SetSize(100, 20)
btn:SetPos(190, 8)
btn:SetFont("Binder_Small")
btn:SetTextColor(Color(255, 255, 255))
btn.Paint = function(s, w, h)
draw.RoundedBox(4, 0, 0, w, h, Color(60, 60, 60))
end
btn.DoClick = function()
local frame = vgui.Create("DFrame")
frame:SetTitle("Select Theme Color")
frame:SetSize(300, 250)
frame:Center()
frame:MakePopup()
local mixer = vgui.Create("DColorMixer", frame)
mixer:Dock(FILL)
mixer:SetPalette(true)
mixer:SetAlphaBar(false)
mixer:SetWangs(true)
mixer:SetColor(Binder.Config.AccentColor)
local apply = vgui.Create("DButton", frame)
apply:Dock(BOTTOM)
apply:SetText("Apply")
apply.DoClick = function()
Binder.Config.AccentColor = mixer:GetColor()
if IsValid(Binder.Frame) then Binder.Frame.AccentColor = Binder.Config.AccentColor end
frame:Remove()
end
end
elseif item.type == "presets" then
local lbl = vgui.Create("DLabel", row)
lbl:SetText(item.label)
lbl:SetFont("Binder_Small")
lbl:SetPos(0, 8)
lbl:SizeToContents()
local presets = {Color(0, 67, 28), Color(52, 152, 219), Color(46, 204, 113), Color(231, 76, 60), Color(155, 89, 182), Color(230, 126, 34)}
for i, col in ipairs(presets) do
local p = vgui.Create("DButton", row)
p:SetSize(20, 20)
p:SetPos(150 + (i-1)*25, 8)
p:SetText("")
p.Paint = function(s, w, h)
draw.RoundedBox(4, 0, 0, w, h, col)
if Binder.Config.AccentColor == col then
surface.SetDrawColor(255, 255, 255)
surface.DrawOutlinedRect(0, 0, w, h, 2)
end
end
p.DoClick = function()
Binder.Config.AccentColor = col
if IsValid(Binder.Frame) then Binder.Frame.AccentColor = col end
end
end
end
end
end
vgui.Register("Binder_SettingsTab", PANEL, "EditablePanel")