AddCSLuaFile("cl_init.lua") AddCSLuaFile("shared.lua") include("shared.lua") -- Регистрация всех сетевых сообщений util.AddNetworkString("SchoolBoard_OpenMenu") util.AddNetworkString("SchoolBoard_SaveLayout") util.AddNetworkString("SchoolBoard_SyncLayout") util.AddNetworkString("SchoolBoard_RequestLayout") util.AddNetworkString("SchoolBoard_Clear_Req") util.AddNetworkString("SchoolBoard_Clear_Do") util.AddNetworkString("SchoolBoard_DrawLine") util.AddNetworkString("SchoolBoard_SavePosition") util.AddNetworkString("SchoolBoard_SyncPosition") util.AddNetworkString("SchoolBoard_SaveRotation") util.AddNetworkString("SchoolBoard_SyncRotation") -- helper: only SAM ranks superadmin/curator (or equivalent usergroups) can modify board local function CanModifyBoard(ply) if not IsValid(ply) or not ply:IsPlayer() then return false end -- standard checks if ply:IsSuperAdmin() then return true end if ply:GetUserGroup() == "curator" then return true end -- if SAM is present, consult its API as a fallback if sam and sam.IsPlayerInRank then if sam.IsPlayerInRank(ply, "superadmin") or sam.IsPlayerInRank(ply, "curator") then return true end end return false end -- append log entries to a file and console local function LogBoardChange(ply, ent, action) local name = IsValid(ply) and ply:Nick() or "Console" local sid = IsValid(ply) and ply:SteamID() or "N/A" local id = IsValid(ent) and ent:EntIndex() or 0 local pos = IsValid(ent) and tostring(ent:GetPos()) or "unknown" local msg = string.format("[%s] %s (%s) %s board #%d at %s\n", os.date(), name, sid, action, id, pos) print(msg) file.Append("schoolboard_changes.txt", msg) end -- console command for printing current log (admins only) concommand.Add("schoolboard_printlog", function(ply, cmd, args) if IsValid(ply) and not CanModifyBoard(ply) then ply:ChatPrint("Нет доступа к логу доски.") return end local contents = file.Read("schoolboard_changes.txt", "DATA") or "" if IsValid(ply) then ply:PrintMessage(HUD_PRINTCONSOLE, contents) ply:ChatPrint("Лог выведен в консоль.") else print(contents) end end) -- очистка лога доски (только для старшего админства) concommand.Add("schoolboard_clearlog", function(ply, cmd, args) if IsValid(ply) and not CanModifyBoard(ply) then ply:ChatPrint("Нет доступа к управлению логом.") return end file.Write("schoolboard_changes.txt", "") if IsValid(ply) then ply:ChatPrint("Лог доски очищен.") ply:PrintMessage(HUD_PRINTCONSOLE, "Лог доски был очищен.") else print("schoolboard_changes.txt was cleared by console") end end) function ENT:Initialize() self:SetModel("models/props/cs_office/offcorkboarda.mdl") self:PhysicsInit(SOLID_VPHYSICS) self:SetMoveType(MOVETYPE_VPHYSICS) self:SetSolid(SOLID_VPHYSICS) self:SetUseType(SIMPLE_USE) -- Позволяет нажимать 'E' на доску local phys = self:GetPhysicsObject() if IsValid(phys) then phys:Wake() end self.BoardLayoutStr = "[]" -- По умолчанию пустой массив JSON self.PositionOffset = Vector(3713.2, 4595.6, -200) -- Смещение позиции доски в 3D пространстве self.RotationOffset = Angle(0, -180, 90) -- Смещение поворота доски end -- Открываем меню настройки, когда игрок нажимает 'E' function ENT:Use(activator, caller) if IsValid(activator) and activator:IsPlayer() then if not CanModifyBoard(activator) then activator:ChatPrint("Только superadmin/curator может редактировать доску.") return end net.Start("SchoolBoard_OpenMenu") net.WriteEntity(self) net.Send(activator) end end -- Клиент отправляет свой новый макет доски (массив картинок и текстов) net.Receive("SchoolBoard_SaveLayout", function(len, ply) local ent = net.ReadEntity() if len > 64000 then -- Ограничение размера пакета на всякий случай return end local layoutSize = net.ReadUInt(32) local compressedData = net.ReadData(layoutSize) if IsValid(ent) and ent:GetClass() == "school_board" then -- Разрешены только superadmin/curator (SAM ранги) или соответствующие usergroup if not CanModifyBoard(ply) then if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end return end -- Проверка расстояния, чтобы школьник с другого конца карты не менял доску if ply:GetPos():DistToSqr(ent:GetPos()) > 500000 then return end ent.BoardLayoutStr = util.Decompress(compressedData) or "[]" -- логируем факт сохранения LogBoardChange(ply, ent, "saved layout") -- Рассказываем всем остальным, что доска обновилась net.Start("SchoolBoard_SyncLayout") net.WriteEntity(ent) net.WriteUInt(layoutSize, 32) net.WriteData(compressedData, layoutSize) net.Broadcast() end end) -- Когда игрок подключается или загружает сущность, он просит её данные net.Receive("SchoolBoard_RequestLayout", function(len, ply) local ent = net.ReadEntity() if IsValid(ent) and ent:GetClass() == "school_board" then if not ent.BoardLayoutStr then ent.BoardLayoutStr = "[]" end local compressedData = util.Compress(ent.BoardLayoutStr) if compressedData then local layoutSize = string.len(compressedData) net.Start("SchoolBoard_SyncLayout") net.WriteEntity(ent) net.WriteUInt(layoutSize, 32) net.WriteData(compressedData, layoutSize) net.Send(ply) end end end) -- Принимаем запрос на очистку от игрока и рассылаем всем net.Receive("SchoolBoard_Clear_Req", function(len, ply) local ent = net.ReadEntity() if IsValid(ent) and ent:GetClass() == "school_board" then if not CanModifyBoard(ply) then if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end return end LogBoardChange(ply, ent, "cleared board") net.Start("SchoolBoard_Clear_Do") net.WriteEntity(ent) net.Broadcast() end end) -- Принимаем координаты линии и цвет от рисующего и рассылаем остальным net.Receive("SchoolBoard_DrawLine", function(len, ply) local ent = net.ReadEntity() local x1 = net.ReadFloat() local y1 = net.ReadFloat() local x2 = net.ReadFloat() local y2 = net.ReadFloat() local r = net.ReadUInt(8) local g = net.ReadUInt(8) local b = net.ReadUInt(8) if IsValid(ent) and ent:GetClass() == "school_board" then -- ограничения на рисование маркером if not CanModifyBoard(ply) then return end -- Проверяем, что игрок стоит рядом с доской if ply:GetPos():DistToSqr(ent:GetPos()) > 50000 then return end net.Start("SchoolBoard_DrawLine", true) -- unreliable пакет net.WriteEntity(ent) net.WriteFloat(x1) net.WriteFloat(y1) net.WriteFloat(x2) net.WriteFloat(y2) net.WriteUInt(r, 8) net.WriteUInt(g, 8) net.WriteUInt(b, 8) net.SendOmit(ply) -- Отправляем всем, кроме рисующего end end) -- Сохраняем позицию доски (смещение вектора) net.Receive("SchoolBoard_SavePosition", function(len, ply) local ent = net.ReadEntity() local offsetX = net.ReadFloat() local offsetY = net.ReadFloat() local offsetZ = net.ReadFloat() if IsValid(ent) and ent:GetClass() == "school_board" then if not CanModifyBoard(ply) then if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end return end -- Проверка расстояния if ply:GetPos():DistToSqr(ent:GetPos()) > 500000 then return end ent.PositionOffset = Vector(offsetX, offsetY, offsetZ) -- логируем изменение позиции LogBoardChange(ply, ent, "changed position") -- Синхронизируем позицию со всеми net.Start("SchoolBoard_SyncPosition") net.WriteEntity(ent) net.WriteFloat(offsetX) net.WriteFloat(offsetY) net.WriteFloat(offsetZ) net.Broadcast() end end) -- Сохраняем поворот доски (смещение угла) net.Receive("SchoolBoard_SaveRotation", function(len, ply) local ent = net.ReadEntity() local pitch = net.ReadFloat() local yaw = net.ReadFloat() local roll = net.ReadFloat() if IsValid(ent) and ent:GetClass() == "school_board" then if not CanModifyBoard(ply) then if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end return end -- Проверка расстояния if ply:GetPos():DistToSqr(ent:GetPos()) > 500000 then return end ent.RotationOffset = Angle(pitch, yaw, roll) -- логируем изменение поворота LogBoardChange(ply, ent, "changed rotation") -- Синхронизируем поворот со всеми net.Start("SchoolBoard_SyncRotation") net.WriteEntity(ent) net.WriteFloat(pitch) net.WriteFloat(yaw) net.WriteFloat(roll) net.Broadcast() end end)