local PLUGIN = PLUGIN util.AddNetworkString("ix.F4_RequestInfo") util.AddNetworkString("ix.F4_SendInfo") util.AddNetworkString("ix.F4_DonatePurchase") util.AddNetworkString("ix.F4_DonatePurchaseResult") util.AddNetworkString("ixChangeName") util.AddNetworkString("ixF4UpdateName") local function GetFactionCounters(factionID, podIndex, specIndex) local factionOnline, samePod, sameSpec = 0, 0, 0 for _, ply in ipairs(player.GetAll()) do if not IsValid(ply) then continue end if ply:Team() ~= factionID then continue end factionOnline = factionOnline + 1 local char = ply:GetCharacter() if not char then continue end if char:GetPodr() == podIndex then samePod = samePod + 1 end if char:GetSpec() == specIndex then sameSpec = sameSpec + 1 end end return factionOnline, samePod, sameSpec end local function BuildInfoPayload(client) local char = client:GetCharacter() if not char then return end local factionID = char:GetFaction() local factionTable = ix.faction.Get(factionID) local podIndex = char:GetPodr() local specIndex = char:GetSpec() local rankIndex = char:GetRank() or 1 local factionOnline, samePod, sameSpec = GetFactionCounters(factionID, podIndex, specIndex) local vehiclesPlugin = ix.plugin.Get("vehicles") local arsenalPlugin = ix.plugin.Get("arsenal") local techPoints = 0 if vehiclesPlugin and vehiclesPlugin.GetFactionPoints then techPoints = vehiclesPlugin:GetFactionPoints(factionID) or 0 end local supplyPoints = 0 if arsenalPlugin and arsenalPlugin.GetFactionSupply then supplyPoints = arsenalPlugin:GetFactionSupply(factionID) or 0 end local activeVehicles = 0 if vehiclesPlugin and vehiclesPlugin.activeVehicles then for _, data in pairs(vehiclesPlugin.activeVehicles) do if not istable(data) then continue end if data.faction == factionID then activeVehicles = activeVehicles + 1 end end end local subdivisionData = { index = podIndex, name = "Не назначено", preset = {}, specDefault = 1, members = samePod, model = "", skin = 0, bodygroups = {} } local specData = { index = specIndex, name = "Не назначено", weapons = {}, members = sameSpec, podr = 0 } local rankData = { index = rankIndex, name = "Без звания" } if factionTable then if factionTable.Podr and factionTable.Podr[podIndex] then local unit = factionTable.Podr[podIndex] subdivisionData.name = unit.name or subdivisionData.name subdivisionData.preset = unit.preset or subdivisionData.preset subdivisionData.specDefault = unit.spec_def or subdivisionData.specDefault subdivisionData.model = unit.model or subdivisionData.model subdivisionData.skin = unit.skin or subdivisionData.skin subdivisionData.bodygroups = unit.bodygroups or subdivisionData.bodygroups end if factionTable.Spec and factionTable.Spec[specIndex] then local spec = factionTable.Spec[specIndex] specData.name = spec.name or specData.name specData.weapons = spec.weapons or specData.weapons specData.podr = spec.podr or specData.podr end if factionTable.Ranks and factionTable.Ranks[rankIndex] then local rank = factionTable.Ranks[rankIndex] rankData.name = rank[1] or rankData.name end end local factionColor = nil if factionTable and factionTable.color then factionColor = { r = factionTable.color.r, g = factionTable.color.g, b = factionTable.color.b } end local supplyStatus = "Стабильно" local minSupply = 0 if arsenalPlugin and arsenalPlugin.config then minSupply = arsenalPlugin.config.minSupply or 0 end if supplyPoints <= minSupply then supplyStatus = "Критически низко" elseif supplyPoints <= minSupply * 2 and minSupply > 0 then supplyStatus = "Низко" end return { timestamp = os.time(), faction = { id = factionID, name = factionTable and factionTable.name or "Неизвестно", description = factionTable and factionTable.description or "", color = factionColor, online = factionOnline, techPoints = techPoints, supplyPoints = supplyPoints, supplyStatus = supplyStatus, activeVehicles = activeVehicles }, character = { name = char:GetName(), money = char:GetMoney(), id = char:GetID() or 0, rank = rankData, subdivision = subdivisionData, spec = specData } } end net.Receive("ix.F4_RequestInfo", function(_, client) if not IsValid(client) then return end local payload = BuildInfoPayload(client) if not payload then return end net.Start("ix.F4_SendInfo") net.WriteTable(payload) net.Send(client) end) local DONATE_PURCHASE_COOLDOWN = 2 PLUGIN.donatePurchaseCooldowns = PLUGIN.donatePurchaseCooldowns or {} local function GetCooldownKey(client) return client:SteamID64() or client:SteamID() or client:EntIndex() end function PLUGIN:GetIGSBalance(client) if not IsValid(client) then return 0 end if client.IGSFunds then local ok, funds = pcall(client.IGSFunds, client) if ok and isnumber(funds) then return math.max(math.floor(funds), 0) end end if client.GetNetVar then local funds = client:GetNetVar("igsFunds", 0) if isnumber(funds) then return math.max(math.floor(funds), 0) end end return 0 end function PLUGIN:AdjustIGSBalance(client, amount) if not IsValid(client) then return false, "Игрок недоступен" end amount = tonumber(amount) or 0 if amount == 0 then return true end if not client.AddIGSFunds then return false, "IGS недоступен" end local ok, err = pcall(client.AddIGSFunds, client, amount) if ok then return true end return false, err or "Ошибка IGS" end function PLUGIN:IsDonateOnCooldown(client) local key = GetCooldownKey(client) local expires = self.donatePurchaseCooldowns[key] or 0 if CurTime() < expires then return true end end function PLUGIN:ArmDonateCooldown(client) local key = GetCooldownKey(client) self.donatePurchaseCooldowns[key] = CurTime() + DONATE_PURCHASE_COOLDOWN end function PLUGIN:CanPurchaseDonateProduct(client, entry) local char = client:GetCharacter() if not char then return false, "Нет активного персонажа" end if not entry then return false, "Предложение не найдено" end if entry.oneTime then local history = char:GetData("donate_purchases", {}) if history[entry.id] then return false, "Этот набор уже был приобретен" end end local hookResult, hookMessage = hook.Run("CanPlayerBuyDonateProduct", client, entry) if hookResult == false then return false, hookMessage or "Покупка отклонена" end return true end function PLUGIN:GrantDonateItems(client, grantData) if not grantData then return end local char = client:GetCharacter() if not char then return end if istable(grantData.weapons) and #grantData.weapons > 0 then local owned = char:GetData("donate_weapons", {}) local changed for _, class in ipairs(grantData.weapons) do if isstring(class) and not table.HasValue(owned, class) then table.insert(owned, class) changed = true end end if changed then char:SetData("donate_weapons", owned) end end if istable(grantData.cosmetics) and #grantData.cosmetics > 0 then local cosmetics = char:GetData("donate_cosmetics", {}) for _, cosmeticID in ipairs(grantData.cosmetics) do if isstring(cosmeticID) then cosmetics[cosmeticID] = true end end char:SetData("donate_cosmetics", cosmetics) end if istable(grantData.vehicles) and #grantData.vehicles > 0 then local vehicles = char:GetData("donate_vehicles", {}) for _, certificate in ipairs(grantData.vehicles) do if isstring(certificate) then vehicles[#vehicles + 1] = certificate end end char:SetData("donate_vehicles", vehicles) end if istable(grantData.boosts) and #grantData.boosts > 0 then local calls = char:GetData("donate_support_calls", {}) for _, callID in ipairs(grantData.boosts) do calls[#calls + 1] = callID end char:SetData("donate_support_calls", calls) end if istable(grantData.items) and #grantData.items > 0 then local pending = char:GetData("donate_items", {}) for _, itemID in ipairs(grantData.items) do pending[#pending + 1] = { id = itemID, ts = os.time() } end char:SetData("donate_items", pending) end end function PLUGIN:ApplyDonateReward(client, entry, mode) local char = client:GetCharacter() if not char then return false, "Нет активного персонажа" end local reward = entry.reward if not reward then local hookResult, hookMessage = hook.Run("OnDonateReward", client, entry, mode) if hookResult ~= nil then return hookResult, hookMessage end return false, "Награда не настроена" end local rewardType = reward.type if rewardType == "privilege" then local tier = reward.tier or entry.id local duration = nil if mode == "3month" then duration = os.time() + (90 * 86400) -- 90 дней else duration = os.time() + (30 * 86400) -- 30 дней (1 месяц) end local privileges = char:GetData("donate_privileges", {}) privileges[tier] = { tier = tier, expires = duration, granted = os.time(), mode = mode or "month" } char:SetData("donate_privileges", privileges) -- Применяем группу через SAM если доступно if sam then local groupMap = { vip = "vip", vip_plus = "vip_plus", premium = "premium", sponsor = "sponsor" } local rank = groupMap[tier] if rank then local expireTime = mode == "3month" and os.time() + (90 * 86400) or os.time() + (30 * 86400) sam.player.set_rank(client, rank, expireTime) end end local modeText = mode == "3month" and "на 3 месяца" or "на 1 месяц" return true, reward.successMessage or ("Привилегия " .. tier .. " активирована " .. modeText) elseif rewardType == "weapon" then local weaponClass = reward.weaponClass or reward.class if not weaponClass then return false, "Класс оружия не указан" end -- Добавляем оружие в список донат-оружия персонажа со сроком действия local donateWeapons = char:GetData("donate_weapons_timed", {}) if not istable(donateWeapons) then donateWeapons = {} end -- Вычисляем срок действия local expireTime if mode == "3month" then expireTime = os.time() + (90 * 86400) -- 90 дней else expireTime = os.time() + (30 * 86400) -- 30 дней end -- Добавляем или обновляем срок donateWeapons[weaponClass] = { expires = expireTime, granted = os.time(), mode = mode or "1month" } char:SetData("donate_weapons_timed", donateWeapons) local modeText = mode == "3month" and "на 3 месяца" or "на 1 месяц" return true, reward.successMessage or ("Донат-оружие " .. (reward.name or weaponClass) .. " добавлено в ваш арсенал " .. modeText) elseif rewardType == "money" then local amount = reward.amount or 0 if amount > 0 and char.GiveMoney then char:GiveMoney(math.floor(amount)) return true, reward.successMessage or ("Вам начислено " .. amount .. " ₽") end return false, "Ошибка начисления денег" elseif rewardType == "voice_chat" then -- Разблокировка голосового чата char:SetData("voice_chat_unlocked", true) -- Если есть система SAM, можно добавить флаг if sam then -- Можно добавить специальную группу или флаг для голосового чата end return true, reward.successMessage or "Голосовой чат разблокирован!" elseif rewardType == "bundle" then if reward.money and reward.money > 0 and char.GiveMoney then char:GiveMoney(math.floor(reward.money)) end if reward.supply and reward.supply ~= 0 then local arsenal = ix.plugin.Get("arsenal") if arsenal and arsenal.AddFactionSupply then arsenal:AddFactionSupply(client:Team(), reward.supply) end end if reward.techPoints and reward.techPoints ~= 0 then local vehicles = ix.plugin.Get("vehicles") if vehicles and vehicles.AddFactionPoints then vehicles:AddFactionPoints(client:Team(), reward.techPoints) end end if reward.grantedItems then self:GrantDonateItems(client, reward.grantedItems) end return true, reward.successMessage or "Комплект успешно выдан" elseif rewardType == "pass" then local tier = reward.tier or entry.id local duration = math.max(1, tonumber(reward.durationDays) or 30) * 86400 local passes = char:GetData("donate_passes", {}) passes[tier] = { tier = tier, expires = os.time() + duration, bonuses = reward.bonuses or {} } char:SetData("donate_passes", passes) return true, reward.successMessage or "Подписка активирована" elseif rewardType == "booster" then local boosterID = reward.boosterID or entry.id local duration = math.max(1, tonumber(reward.durationHours) or 24) * 3600 local boosters = char:GetData("donate_boosters", {}) boosters[boosterID] = { expires = os.time() + duration, multiplier = reward.multiplier or 1.0 } char:SetData("donate_boosters", boosters) return true, reward.successMessage or "Бустер активирован" elseif rewardType == "case" then local caseID = reward.caseID or entry.id local cases = char:GetData("donate_cases", {}) cases[caseID] = (cases[caseID] or 0) + 1 char:SetData("donate_cases", cases) return true, reward.successMessage or "Кейс добавлен в коллекцию" elseif rewardType == "cosmetic" then local unlockID = reward.unlockID or entry.id local cosmetics = char:GetData("donate_cosmetics", {}) cosmetics[unlockID] = true char:SetData("donate_cosmetics", cosmetics) return true, reward.successMessage or "Косметика разблокирована" end local hookResult, hookMessage = hook.Run("OnDonateReward", client, entry) if hookResult ~= nil then return hookResult, hookMessage end return false, "Тип награды не поддерживается" end function PLUGIN:MarkDonatePurchase(client, entry) local char = client:GetCharacter() if not char then return end local history = char:GetData("donate_purchases", {}) history[entry.id] = (history[entry.id] or 0) + 1 char:SetData("donate_purchases", history) end function PLUGIN:SendDonateResult(client, success, message, productID) if not IsValid(client) then return end net.Start("ix.F4_DonatePurchaseResult") net.WriteBool(success and true or false) net.WriteString(message or "") net.WriteString(productID or "") net.Send(client) end function PLUGIN:HandleDonatePurchase(client, productID, price, mode) local entry = self:GetDonateProduct(productID) local ok, msg = self:CanPurchaseDonateProduct(client, entry) if not ok then return false, msg, entry end local actualPrice = price or tonumber(entry and entry.price) or 0 if actualPrice <= 0 then return false, "Цена предложения не настроена", entry end local funds = self:GetIGSBalance(client) if funds < actualPrice then return false, "Недостаточно средств на балансе", entry end local debited, debitError = self:AdjustIGSBalance(client, -actualPrice) if not debited then return false, debitError or "Не удалось списать средства", entry end local success, rewardMessage = self:ApplyDonateReward(client, entry, mode) if not success then self:AdjustIGSBalance(client, actualPrice) return false, rewardMessage or "Ошибка выдачи награды", entry end self:MarkDonatePurchase(client, entry, mode) hook.Run("PostDonatePurchase", client, entry, actualPrice, mode) return true, rewardMessage or "Покупка завершена", entry end net.Receive("ix.F4_DonatePurchase", function(_, client) if not IsValid(client) or not client:IsPlayer() then return end local productID = net.ReadString() or "" local price = net.ReadUInt(32) or 0 local mode = net.ReadString() or "month" if productID == "" then return end if PLUGIN:IsDonateOnCooldown(client) then PLUGIN:SendDonateResult(client, false, "Слишком частые запросы", productID) return end PLUGIN:ArmDonateCooldown(client) local success, message, entry = PLUGIN:HandleDonatePurchase(client, productID, price, mode) local entryID = entry and entry.id or productID if success then local priceText = tostring(price) local modeText = mode == "forever" and " (навсегда)" or " (на месяц)" print(string.format("[F4 Donate] %s (%s) купил %s%s за %s", client:Name(), client:SteamID(), entryID, modeText, priceText)) else print(string.format("[F4 Donate] Ошибка покупки %s игроком %s: %s", entryID, client:Name(), tostring(message))) end PLUGIN:SendDonateResult(client, success, message, entryID) end) -- Admin command to add IGS funds ix.command.Add("GiveIGS", { description = "Выдать IGS средства игроку", CanRun = function(self, client) local userGroup = string.lower(client:GetUserGroup() or "user") return client:IsAdmin() or AdminPrivs[userGroup] end, arguments = { ix.type.player, ix.type.number }, OnRun = function(self, client, target, amount) if not IsValid(target) then return "@invalidTarget" end amount = math.floor(tonumber(amount) or 0) if amount == 0 then return "Укажите корректную сумму" end local success, err = PLUGIN:AdjustIGSBalance(target, amount) if success then local action = amount > 0 and "выдал" or "снял" client:Notify(string.format("Вы %s %d IGS для %s", action, math.abs(amount), target:Name())) target:Notify(string.format("Администратор %s вам %d IGS", action == "выдал" and "выдал" or "снял у вас", math.abs(amount))) print(string.format("[IGS Admin] %s (%s) %s %d IGS для %s (%s)", client:Name(), client:SteamID(), action, math.abs(amount), target:Name(), target:SteamID())) return "" else return err or "Ошибка при изменении баланса" end end }) -- Admin command to check IGS balance ix.command.Add("CheckIGS", { description = "Проверить IGS баланс игрока", CanRun = function(self, client) local userGroup = string.lower(client:GetUserGroup() or "user") return client:IsAdmin() or AdminPrivs[userGroup] end, arguments = { ix.type.player }, OnRun = function(self, client, target) if not IsValid(target) then return "@invalidTarget" end local balance = PLUGIN:GetIGSBalance(target) return string.format("Баланс IGS игрока %s: %d руб.", target:Name(), balance) end }) net.Receive("ixChangeName", function(len, client) if not IsValid(client) then return end local character = client:GetCharacter() if not character then return end local newName = net.ReadString() if not newName or newName == "" then return end if #newName < 3 or #newName > 50 then client:Notify("Имя должно содержать от 3 до 50 символов") return end if newName:find("[<>\"\\/]") then client:Notify("Имя содержит недопустимые символы") return end local oldName = character:GetName() character:SetName(newName) client:Notify("Ваше имя успешно изменено на: " .. newName) net.Start("ixF4UpdateName") net.WriteString(newName) net.Send(client) hook.Run("OnCharacterVarChanged", character, "name", oldName, newName) end) -- Фикс выдачи донат-оружия IGS в Helix hook.Add("PostPlayerLoadout", "IGS_Helix_WeaponLoadoutFix", function(client) if not IsValid(client) or not IGS then return end timer.Simple(1.5, function() if not IsValid(client) or not client:GetCharacter() then return end if isfunction(IGS.PlayerLoadout) then IGS.PlayerLoadout(client) end if isfunction(IGS.GetItems) and IsValid(client) and client.HasPurchase then for _, ITEM in pairs(IGS.GetItems()) do if not ITEM or not isfunction(ITEM.GetUID) then continue end local weaponClass = (isfunction(ITEM.GetWeapon) and ITEM:GetWeapon()) or ITEM.weapon if weaponClass and client:HasPurchase(ITEM:GetUID()) then if not client:HasWeapon(weaponClass) then client:Give(weaponClass) end end end end end) end)