From 591f2cd4452f700905380aa6a5c1c5b5fd6f9ae5 Mon Sep 17 00:00:00 2001 From: AlexALX Date: Thu, 30 Jul 2026 14:54:24 +0300 Subject: [PATCH] Add wire_digitalscreen_rate convar This convar multiply all digital screen limits in sync, so wire_digitalscreen_rate 10 make it work exactly 10x faster. --- .../gmod_wire_digitalscreen/cl_init.lua | 726 +++++++-------- lua/entities/gmod_wire_digitalscreen/init.lua | 876 +++++++++--------- 2 files changed, 807 insertions(+), 795 deletions(-) diff --git a/lua/entities/gmod_wire_digitalscreen/cl_init.lua b/lua/entities/gmod_wire_digitalscreen/cl_init.lua index ae6aedef67..a4f033f61e 100644 --- a/lua/entities/gmod_wire_digitalscreen/cl_init.lua +++ b/lua/entities/gmod_wire_digitalscreen/cl_init.lua @@ -1,363 +1,363 @@ -include("shared.lua") - - - -function ENT:SendData() - net.Start("wire_interactiveprop_action") - - local data = WireLib.GetInteractiveModel(self:GetModel()).widgets - net.WriteEntity(self) - for i=1, #data do - net.WriteFloat(self.InteractiveData[i]) - end - net.SendToServer() -end - -function ENT:GetPanel() - if not self.IsInteractive then return end - local data = WireLib.GetInteractiveModel(self:GetModel()) - return WireLib.GetInteractiveWidgetBody(self, data) -end - - -function ENT:AddButton(id,button) - if not self.IsInteractive then return end - self.Buttons[id] = button -end - -function ENT:Initialize() - self.Memory1 = {} - self.Memory2 = {} - - self.InteractiveData = {} - self.LastButtons = {} - self.Buttons = {} - local interactive_model = WireLib.GetInteractiveModel(self:GetModel()) - self.IsInteractive = false - if interactive_model then - self.IsInteractive = true - for i=1, #WireLib.GetInteractiveModel(self:GetModel()).widgets do - self.InteractiveData[i] = 0 - end - end - - self.LastClk = true - self.NewClk = true - self.Memory1[1048575] = 1 - self.Memory2[1048575] = 1 - self.NeedRefresh = true - self.IsClear = true - self.ClearQueued = false - self.RefreshPixels = {} - self.RefreshRows = {} - - self.ScreenWidth = 32 - self.ScreenHeight = 32 - - for i=1,self.ScreenHeight do - self.RefreshRows[i] = i-1 - end - - --0..786431 - RGB data - - --1048569 - Color mode (0: RGBXXX; 1: R G B) - --1048570 - Clear row - --1048571 - Clear column - --1048572 - Screen Height - --1048573 - Screen Width - --1048574 - Hardware Clear Screen - --1048575 - CLK - - self.GPU = WireGPU(self) - - self.buffer = {} - - WireLib.netRegister(self) -end - -function ENT:OnRemove() - self.GPU:Finalize() - self.NeedRefresh = true -end - -local function stringToNumber(index, str, bytes) - local newpos = index+bytes - str = str:sub(index,newpos-1) - local n = 0 - for j=1,bytes do - n = n + str:byte(j)*(256^(j-1)) - end - return n, newpos -end - -local pixelbits = {3, 1, 3, 4, 1} -net.Receive("wire_digitalscreen", function() - local ent = Entity(net.ReadUInt(16)) - - if IsValid(ent) and ent.Memory1 and ent.Memory2 then - local pixelbit = pixelbits[net.ReadUInt(5)] - local len = net.ReadUInt(32) - local datastr = util.Decompress(net.ReadData(len)) - if #datastr>0 then - ent:AddBuffer(datastr,pixelbit) - end - end -end) - -function ENT:AddBuffer(datastr,pixelbit) - self.buffer[#self.buffer+1] = {datastr=datastr,readIndex=1,pixelbit=pixelbit} -end -function ENT:ProcessBuffer() - if not self.buffer[1] then return end - - local datastr = self.buffer[1].datastr - local readIndex = self.buffer[1].readIndex - local pixelbit = self.buffer[1].pixelbit - - local length - length, readIndex = stringToNumber(readIndex,datastr,3) - if length == 0 then - table.remove( self.buffer, 1 ) - return false - end - local address - address, readIndex = stringToNumber(readIndex,datastr,3) - - for i = address, address + length - 1 do - if i>=1048500 then - local data - data, readIndex = stringToNumber(readIndex,datastr,2) - self:WriteCell(i, data) - else - local data - data, readIndex = stringToNumber(readIndex,datastr,pixelbit) - self:WriteCell(i, data) - end - - coroutine.yield(true) - end - - self.buffer[1].readIndex = readIndex - return false -end - -function ENT:Think() - if self.buffer[1] ~= nil then - local maxtime = SysTime() + RealFrameTime() * 0.05 -- do more depending on client FPS. Higher fps = more work - - while SysTime() < maxtime and self.buffer[1] do - if not self.co or coroutine.status(self.co) == "dead" then - self.co = coroutine.create( function() - self:ProcessBuffer() - end ) - end - - coroutine.resume(self.co) - end - end - - self:NextThink(CurTime()+0.1) - return true -end - -function ENT:ReadCell(Address,value) - Address = math.floor(Address) - if Address < 0 then return nil end - if Address >= 1048577 then return nil end - - return self.Memory2[Address] -end - -function ENT:WriteCell(Address,value) - Address = math.floor(Address) - if Address < 0 then return false end - if Address >= 1048577 then return false end - - if Address == 1048575 then - self.NewClk = value ~= 0 - elseif Address < 1048500 then - self.IsClear = false - end - - if (self.NewClk) then - self.Memory1[Address] = value -- visible buffer - self.NeedRefresh = true - if self.Memory1[1048569] == 1 then -- R G B mode - local pixelno = math.floor(Address/3) - if self.RefreshPixels[#self.RefreshPixels] ~= pixelno then - self.RefreshPixels[#self.RefreshPixels+1] = pixelno - end - else -- other modes - self.RefreshPixels[#self.RefreshPixels+1] = Address - end - end - self.Memory2[Address] = value -- invisible buffer - - if Address == 1048574 then -- Hardware Clear Screen - local mem1,mem2 = {},{} - for addr = 1048500,1048575 do - mem1[addr] = self.Memory1[addr] - mem2[addr] = self.Memory2[addr] - end - self.Memory1,self.Memory2 = mem1,mem2 - self.IsClear = true - self.ClearQueued = true - self.NeedRefresh = true - self.RefreshRows = {} - elseif Address == 1048572 then - self.ScreenHeight = value - if not self.IsClear then - self.NeedRefresh = true - for i = 1,self.ScreenHeight do - self.RefreshRows[i] = i-1 - end - end - elseif Address == 1048573 then - self.ScreenWidth = value - if not self.IsClear then - self.NeedRefresh = true - for i = 1,self.ScreenHeight do - self.RefreshRows[i] = i-1 - end - end - end - - if self.LastClk ~= self.NewClk then - -- swap the memory if clock changes - self.LastClk = self.NewClk - self.Memory1 = table.Copy(self.Memory2) - - self.NeedRefresh = true - for i=1,self.ScreenHeight do - self.RefreshRows[i] = i-1 - end - end - return true -end - -local transformcolor = {} -transformcolor[0] = function(c) -- RGBXXX - local crgb = math.floor(c / 1000) - local cgray = c - math.floor(c / 1000)*1000 - - cb = cgray+28*math.fmod(crgb, 10) - cg = cgray+28*math.fmod(math.floor(crgb / 10), 10) - cr = cgray+28*math.fmod(math.floor(crgb / 100), 10) - - return cr, cg, cb -end -transformcolor[2] = function(c) -- 24 bit mode - cb = math.fmod(c, 256) - cg = math.fmod(math.floor(c / 256), 256) - cr = math.fmod(math.floor(c / 65536), 256) - - return cr, cg, cb -end -transformcolor[3] = function(c) -- RRRGGGBBB - cb = math.fmod(c, 1000) - cg = math.fmod(math.floor(c / 1e3), 1000) - cr = math.fmod(math.floor(c / 1e6), 1000) - - return cr, cg, cb -end -transformcolor[4] = function(c) -- XXX - return c, c, c -end - -function ENT:RedrawPixel(a) - if a >= self.ScreenWidth*self.ScreenHeight then return end - - local cr,cg,cb - - local x = a % self.ScreenWidth - local y = math.floor(a / self.ScreenWidth) - - local colormode = self.Memory1[1048569] or 0 - - if colormode == 1 then - cr = self.Memory1[a*3 ] or 0 - cg = self.Memory1[a*3+1] or 0 - cb = self.Memory1[a*3+2] or 0 - else - local c = self.Memory1[a] or 0 - cr, cg, cb = (transformcolor[colormode] or transformcolor[0])(c) - end - - - surface.SetDrawColor(cr,cg,cb,255) - surface.DrawRect( x, y, 1, 1 ) -end - -function ENT:RedrawRow(y) - if y >= self.ScreenHeight then return end - local a = y*self.ScreenWidth - - local colormode = self.Memory1[1048569] or 0 - - for x = 0,self.ScreenWidth-1 do - local cr,cg,cb - - if (colormode == 1) then - cr = self.Memory1[(a+x)*3 ] or 0 - cg = self.Memory1[(a+x)*3+1] or 0 - cb = self.Memory1[(a+x)*3+2] or 0 - else - local c = self.Memory1[a+x] or 0 - cr, cg, cb = (transformcolor[colormode] or transformcolor[0])(c) - end - - surface.SetDrawColor(cr,cg,cb,255) - surface.DrawRect( x, y, 1, 1 ) - end -end - -local VECTOR_1_1_1 = Vector(1, 1, 1) -function ENT:Draw(flags) - self:DrawModel(flags) - - local tone = render.GetToneMappingScaleLinear() - render.SetToneMappingScaleLinear(VECTOR_1_1_1) - - if self.NeedRefresh then - self.NeedRefresh = false - local maxtime = SysTime() + RealFrameTime() * 0.01 - - self.GPU:RenderToGPU(function() - local idx = 0 - - if self.ClearQueued then - surface.SetDrawColor(0,0,0,255) - surface.DrawRect(0,0, 1024,1024) - self.ClearQueued = false - return - end - - if (#self.RefreshRows > 0) then - idx = #self.RefreshRows - while ((idx > 0) and (SysTime() < maxtime)) do - self:RedrawRow(self.RefreshRows[idx]) - self.RefreshRows[idx] = nil - idx = idx - 1 - end - else - idx = #self.RefreshPixels - while ((idx > 0) and (SysTime() < maxtime)) do - self:RedrawPixel(self.RefreshPixels[idx]) - self.RefreshPixels[idx] = nil - idx = idx - 1 - end - end - if idx ~= 0 then - self.NeedRefresh = true - end - end) - end - - self.GPU:Render(0,0,1024,1024,nil,-(1024-self.ScreenWidth)/1024,-(1024-self.ScreenHeight)/1024) - render.SetToneMappingScaleLinear(tone) - Wire_Render(self) -end - -function ENT:IsTranslucent() - return true -end +include("shared.lua") + +local dsRate = GetConVar("wire_digitalscreen_rate") + +function ENT:SendData() + net.Start("wire_interactiveprop_action") + + local data = WireLib.GetInteractiveModel(self:GetModel()).widgets + net.WriteEntity(self) + for i=1, #data do + net.WriteFloat(self.InteractiveData[i]) + end + net.SendToServer() +end + +function ENT:GetPanel() + if not self.IsInteractive then return end + local data = WireLib.GetInteractiveModel(self:GetModel()) + return WireLib.GetInteractiveWidgetBody(self, data) +end + + +function ENT:AddButton(id,button) + if not self.IsInteractive then return end + self.Buttons[id] = button +end + +function ENT:Initialize() + self.Memory1 = {} + self.Memory2 = {} + + self.InteractiveData = {} + self.LastButtons = {} + self.Buttons = {} + local interactive_model = WireLib.GetInteractiveModel(self:GetModel()) + self.IsInteractive = false + if interactive_model then + self.IsInteractive = true + for i=1, #WireLib.GetInteractiveModel(self:GetModel()).widgets do + self.InteractiveData[i] = 0 + end + end + + self.LastClk = true + self.NewClk = true + self.Memory1[1048575] = 1 + self.Memory2[1048575] = 1 + self.NeedRefresh = true + self.IsClear = true + self.ClearQueued = false + self.RefreshPixels = {} + self.RefreshRows = {} + + self.ScreenWidth = 32 + self.ScreenHeight = 32 + + for i=1,self.ScreenHeight do + self.RefreshRows[i] = i-1 + end + + --0..786431 - RGB data + + --1048569 - Color mode (0: RGBXXX; 1: R G B) + --1048570 - Clear row + --1048571 - Clear column + --1048572 - Screen Height + --1048573 - Screen Width + --1048574 - Hardware Clear Screen + --1048575 - CLK + + self.GPU = WireGPU(self) + + self.buffer = {} + + WireLib.netRegister(self) +end + +function ENT:OnRemove() + self.GPU:Finalize() + self.NeedRefresh = true +end + +local function stringToNumber(index, str, bytes) + local newpos = index+bytes + str = str:sub(index,newpos-1) + local n = 0 + for j=1,bytes do + n = n + str:byte(j)*(256^(j-1)) + end + return n, newpos +end + +local pixelbits = {3, 1, 3, 4, 1} +net.Receive("wire_digitalscreen", function() + local ent = Entity(net.ReadUInt(16)) + + if IsValid(ent) and ent.Memory1 and ent.Memory2 then + local pixelbit = pixelbits[net.ReadUInt(5)] + local len = net.ReadUInt(32) + local datastr = util.Decompress(net.ReadData(len)) + if #datastr>0 then + ent:AddBuffer(datastr,pixelbit) + end + end +end) + +function ENT:AddBuffer(datastr,pixelbit) + self.buffer[#self.buffer+1] = {datastr=datastr,readIndex=1,pixelbit=pixelbit} +end +function ENT:ProcessBuffer() + if not self.buffer[1] then return end + + local datastr = self.buffer[1].datastr + local readIndex = self.buffer[1].readIndex + local pixelbit = self.buffer[1].pixelbit + + local length + length, readIndex = stringToNumber(readIndex,datastr,3) + if length == 0 then + table.remove( self.buffer, 1 ) + return false + end + local address + address, readIndex = stringToNumber(readIndex,datastr,3) + + for i = address, address + length - 1 do + if i>=1048500 then + local data + data, readIndex = stringToNumber(readIndex,datastr,2) + self:WriteCell(i, data) + else + local data + data, readIndex = stringToNumber(readIndex,datastr,pixelbit) + self:WriteCell(i, data) + end + + coroutine.yield(true) + end + + self.buffer[1].readIndex = readIndex + return false +end + +function ENT:Think() + if self.buffer[1] ~= nil then + local maxtime = SysTime() + RealFrameTime() * (0.05*dsRate:GetFloat()) -- do more depending on client FPS. Higher fps = more work + + while SysTime() < maxtime and self.buffer[1] do + if not self.co or coroutine.status(self.co) == "dead" then + self.co = coroutine.create( function() + self:ProcessBuffer() + end ) + end + + coroutine.resume(self.co) + end + end + + self:NextThink(CurTime()+0.1) + return true +end + +function ENT:ReadCell(Address,value) + Address = math.floor(Address) + if Address < 0 then return nil end + if Address >= 1048577 then return nil end + + return self.Memory2[Address] +end + +function ENT:WriteCell(Address,value) + Address = math.floor(Address) + if Address < 0 then return false end + if Address >= 1048577 then return false end + + if Address == 1048575 then + self.NewClk = value ~= 0 + elseif Address < 1048500 then + self.IsClear = false + end + + if (self.NewClk) then + self.Memory1[Address] = value -- visible buffer + self.NeedRefresh = true + if self.Memory1[1048569] == 1 then -- R G B mode + local pixelno = math.floor(Address/3) + if self.RefreshPixels[#self.RefreshPixels] ~= pixelno then + self.RefreshPixels[#self.RefreshPixels+1] = pixelno + end + else -- other modes + self.RefreshPixels[#self.RefreshPixels+1] = Address + end + end + self.Memory2[Address] = value -- invisible buffer + + if Address == 1048574 then -- Hardware Clear Screen + local mem1,mem2 = {},{} + for addr = 1048500,1048575 do + mem1[addr] = self.Memory1[addr] + mem2[addr] = self.Memory2[addr] + end + self.Memory1,self.Memory2 = mem1,mem2 + self.IsClear = true + self.ClearQueued = true + self.NeedRefresh = true + self.RefreshRows = {} + elseif Address == 1048572 then + self.ScreenHeight = value + if not self.IsClear then + self.NeedRefresh = true + for i = 1,self.ScreenHeight do + self.RefreshRows[i] = i-1 + end + end + elseif Address == 1048573 then + self.ScreenWidth = value + if not self.IsClear then + self.NeedRefresh = true + for i = 1,self.ScreenHeight do + self.RefreshRows[i] = i-1 + end + end + end + + if self.LastClk ~= self.NewClk then + -- swap the memory if clock changes + self.LastClk = self.NewClk + self.Memory1 = table.Copy(self.Memory2) + + self.NeedRefresh = true + for i=1,self.ScreenHeight do + self.RefreshRows[i] = i-1 + end + end + return true +end + +local transformcolor = {} +transformcolor[0] = function(c) -- RGBXXX + local crgb = math.floor(c / 1000) + local cgray = c - math.floor(c / 1000)*1000 + + cb = cgray+28*math.fmod(crgb, 10) + cg = cgray+28*math.fmod(math.floor(crgb / 10), 10) + cr = cgray+28*math.fmod(math.floor(crgb / 100), 10) + + return cr, cg, cb +end +transformcolor[2] = function(c) -- 24 bit mode + cb = math.fmod(c, 256) + cg = math.fmod(math.floor(c / 256), 256) + cr = math.fmod(math.floor(c / 65536), 256) + + return cr, cg, cb +end +transformcolor[3] = function(c) -- RRRGGGBBB + cb = math.fmod(c, 1000) + cg = math.fmod(math.floor(c / 1e3), 1000) + cr = math.fmod(math.floor(c / 1e6), 1000) + + return cr, cg, cb +end +transformcolor[4] = function(c) -- XXX + return c, c, c +end + +function ENT:RedrawPixel(a) + if a >= self.ScreenWidth*self.ScreenHeight then return end + + local cr,cg,cb + + local x = a % self.ScreenWidth + local y = math.floor(a / self.ScreenWidth) + + local colormode = self.Memory1[1048569] or 0 + + if colormode == 1 then + cr = self.Memory1[a*3 ] or 0 + cg = self.Memory1[a*3+1] or 0 + cb = self.Memory1[a*3+2] or 0 + else + local c = self.Memory1[a] or 0 + cr, cg, cb = (transformcolor[colormode] or transformcolor[0])(c) + end + + + surface.SetDrawColor(cr,cg,cb,255) + surface.DrawRect( x, y, 1, 1 ) +end + +function ENT:RedrawRow(y) + if y >= self.ScreenHeight then return end + local a = y*self.ScreenWidth + + local colormode = self.Memory1[1048569] or 0 + + for x = 0,self.ScreenWidth-1 do + local cr,cg,cb + + if (colormode == 1) then + cr = self.Memory1[(a+x)*3 ] or 0 + cg = self.Memory1[(a+x)*3+1] or 0 + cb = self.Memory1[(a+x)*3+2] or 0 + else + local c = self.Memory1[a+x] or 0 + cr, cg, cb = (transformcolor[colormode] or transformcolor[0])(c) + end + + surface.SetDrawColor(cr,cg,cb,255) + surface.DrawRect( x, y, 1, 1 ) + end +end + +local VECTOR_1_1_1 = Vector(1, 1, 1) +function ENT:Draw(flags) + self:DrawModel(flags) + + local tone = render.GetToneMappingScaleLinear() + render.SetToneMappingScaleLinear(VECTOR_1_1_1) + + if self.NeedRefresh then + self.NeedRefresh = false + local maxtime = SysTime() + RealFrameTime() * (0.01*dsRate:GetFloat()) + + self.GPU:RenderToGPU(function() + local idx = 0 + + if self.ClearQueued then + surface.SetDrawColor(0,0,0,255) + surface.DrawRect(0,0, 1024,1024) + self.ClearQueued = false + return + end + + if (#self.RefreshRows > 0) then + idx = #self.RefreshRows + while ((idx > 0) and (SysTime() < maxtime)) do + self:RedrawRow(self.RefreshRows[idx]) + self.RefreshRows[idx] = nil + idx = idx - 1 + end + else + idx = #self.RefreshPixels + while ((idx > 0) and (SysTime() < maxtime)) do + self:RedrawPixel(self.RefreshPixels[idx]) + self.RefreshPixels[idx] = nil + idx = idx - 1 + end + end + if idx ~= 0 then + self.NeedRefresh = true + end + end) + end + + self.GPU:Render(0,0,1024,1024,nil,-(1024-self.ScreenWidth)/1024,-(1024-self.ScreenHeight)/1024) + render.SetToneMappingScaleLinear(tone) + Wire_Render(self) +end + +function ENT:IsTranslucent() + return true +end diff --git a/lua/entities/gmod_wire_digitalscreen/init.lua b/lua/entities/gmod_wire_digitalscreen/init.lua index 3be26fc1ee..27bae6842d 100644 --- a/lua/entities/gmod_wire_digitalscreen/init.lua +++ b/lua/entities/gmod_wire_digitalscreen/init.lua @@ -1,432 +1,444 @@ -AddCSLuaFile( "cl_init.lua" ) -AddCSLuaFile( "shared.lua" ) -include("shared.lua") -DEFINE_BASECLASS( "base_wire_entity" ) - -ENT.WireDebugName = "DigitalScreen" - - -function ENT:InitInteractive() - local model = self:GetModel() - local outputs = {"Memory"} - local interactivemodel = WireLib.GetInteractiveModel(model) - for i=1, #interactivemodel.widgets do - outputs[i+1] = interactivemodel.widgets[i].name - end - self.BlockInput = false - self.NextPrompt = 0 - self.Outputs=WireLib.CreateOutputs(self,outputs) - self.IsInteractive = true - self:UpdateOverlay() -end - - -function ENT:ReceiveData() - if not self.IsInteractive then return end - local data = WireLib.GetInteractiveModel(self:GetModel()).widgets - for i = 1, #data do - WireLib.TriggerOutput(self, data[i].name, net.ReadFloat()) - end -end - -function ENT:UpdateOverlay() - if not self.IsInteractive then - return - end - - txt = "" - if IsValid(self.User) then - txt = "In use by: " .. self.User:Nick() - end - - self:SetOverlayText(txt) -end - - -function ENT:Prompt( ply ) - if not self.IsInteractive then return end - if ply then - if CurTime() < self.NextPrompt then return end -- anti spam - self.NextPrompt = CurTime() + 0.1 - - if IsValid( self.User ) then - WireLib.AddNotify(ply,"That interactive prop is in use by another player!",NOTIFY_ERROR,5,6) - return - end - - self.User = ply - - net.Start( "wire_interactiveprop_show" ) - net.WriteEntity( self ) - net.Send( ply ) - else - self:Prompt( self:GetPlayer() ) -- prompt for owner - end -end - -function ENT:Use(ply) - if not IsValid( ply ) then return end - self:Prompt( ply ) -end - -function ENT:Unprompt() - if not self.IsInteractive then return end - self.User = nil -end - -function ENT:Initialize() - - self:PhysicsInit(SOLID_VPHYSICS) - self:SetMoveType(MOVETYPE_VPHYSICS) - self:SetSolid(SOLID_VPHYSICS) - - self.Inputs = Wire_CreateInputs(self, { "PixelX", "PixelY", "PixelG", "Clk", "FillColor", "ClearRow", "ClearCol" }) - if WireLib.IsValidInteractiveModel(self:GetModel()) then - self:InitInteractive() - else - self.Outputs = WireLib.CreateOutputs(self, { "Memory" }) - end - - self.Memory = {} - - self.PixelX = 0 - self.PixelY = 0 - self.PixelG = 0 - self.Memory[1048569] = 0 - self.Memory[1048572] = 32 - self.Memory[1048573] = 32 - self.Memory[1048575] = 1 - - self.ScreenWidth = 32 - self.ScreenHeight = 32 - - self.NumOfWrites = 0 - self.UpdateRate = 0.1 - - self.ChangedCellRanges = {} - self.ChangedStep = 1 -end - -function ENT:Setup(ScreenWidth, ScreenHeight, IsInteractive) - self:WriteCell(1048572, ScreenHeight or 32) - self:WriteCell(1048573, ScreenWidth or 32) - self.IsInteractive = WireLib.IsValidInteractiveModel(self:GetModel()) and (IsInteractive == 1) -end - -function ENT:SendPixel() - if self.Memory[1048575] == 0 then return end -- why? - if self.PixelX < 0 then return end - if self.PixelY < 0 then return end - if self.PixelX >= self.ScreenWidth then return end - if self.PixelY >= self.ScreenHeight then return end - - local address = self.PixelY*self.ScreenWidth + self.PixelX - self:WriteCell(address, self.PixelG) -end - -function ENT:ReadCell(Address) - Address = math.floor(Address) - if Address < 0 then return nil end - if Address >= 1048577 then return nil end - - return self.Memory[Address] or 0 -end - -function ENT:MarkCellChanged(Address) - self.NumOfWrites = self.NumOfWrites + 1 - - local lastrange = self.ChangedCellRanges[#self.ChangedCellRanges] - if lastrange then - if Address == lastrange.start + lastrange.length then - -- wrote just after the end of the range, append - lastrange.length = lastrange.length + 1 - elseif Address == lastrange.start - 1 then - -- wrote just before the start of the range, prepend - lastrange.start = lastrange.start - 1 - lastrange.length = lastrange.length + 1 - elseif Address < lastrange.start - 1 or Address > lastrange.start + lastrange.length then - -- wrote outside the range - lastrange = nil - end - end - if not lastrange then - lastrange = { - start = Address, - length = 1 - } - self.ChangedCellRanges[#self.ChangedCellRanges + 1] = lastrange - end -end - -local function numberToString(t, number, bytes) - local str = {} - for j=1,bytes do - str[j] = string.char(number % 256) - number = math.floor(number / 256) - end - t[#t+1] = table.concat(str) -end - ----------------------------------------------------- --- Processing limiters and global bandwidth limiters -local maxProcessingTime = engine.TickInterval() * 0.9 -local defaultMaxBandwidth = 10000 -- 10k per screen max limit - is arbitrary. needs to be smaller than the global limit. -local defaultMaxGlobalBandwidth = 20000 -- 20k is a good global limit in my testing. higher than that seems to cause issues -local maxBandwidth = defaultMaxBandwidth -local globalBandwidthLookup = {} -local function calcGlobalBW() - maxBandwidth = defaultMaxGlobalBandwidth - local n = 0 - - -- count the number of digi screens currently sending data - for digi in pairs(globalBandwidthLookup) do - if not IsValid(digi) then globalBandwidthLookup[digi] = nil end -- this most likely won't trigger due to OnRemove, but just in case - n = n + 1 - end - - -- player count also seems to affect lag somewhat - -- it seems logical that this would have something to do with the upload bandwidth of the server - -- but that seems unlikely since testing shows that the amount of data sent isn't very high - -- it's more likely that the net message library just isn't very efficient - -- the numbers here are picked somewhat arbitrarily, with a bit of guessing. - -- change in the future if necessary. - n = n + math.max(0,player.GetCount()-2) / 4 - - -- during testing, lag seems to increase somewhat faster as more net messages are sent at the same time - -- so we double this value to compensate - n = n * 2 - - maxBandwidth = math.max(100,math.Round(math.min(defaultMaxBandwidth,maxBandwidth / n),2)) -end -local function addGlobalBW(e) - globalBandwidthLookup[e] = true - calcGlobalBW() -end -local function removeGlobalBW(e) globalBandwidthLookup[e] = nil end ----------------------------------------------------- - -function ENT:OnRemove() - BaseClass.OnRemove(self) - removeGlobalBW(self) -end - -local function buildData(datastr, memory, pixelbit, range, bytesRemaining, sTime) - if bytesRemaining < 15 then return 0 end - local lengthIndex = #datastr+1 - datastr[lengthIndex] = "000" - numberToString(datastr,range.start,3) -- Address of range - bytesRemaining = bytesRemaining - 6 - local i, iend = range.start, range.start + range.length - while i0 and SysTime() - sTime < maxProcessingTime do - if i>=1048500 then - numberToString(datastr,memory[i],2) - bytesRemaining = bytesRemaining - 2 - else - numberToString(datastr,memory[i],pixelbit) - bytesRemaining = bytesRemaining - pixelbit - end - i = i + 1 - end - local lengthStr = {} - numberToString(lengthStr,i - range.start,3) -- Length of range - datastr[lengthIndex] = lengthStr[1] - range.length = iend - i - range.start = i - - return bytesRemaining -end - -util.AddNetworkString("wire_digitalscreen") - - -local pixelbits = {3, 1, 3, 4, 1} --The compressed pixel formats are in bytes -function ENT:FlushCache(ply) - if not next(self.ChangedCellRanges) then - removeGlobalBW(self) - return - end - - local pixelformat = (math.floor(self.Memory[1048569]) or 0) + 1 - if pixelformat < 1 or pixelformat > #pixelbits then pixelformat = 1 end - local pixelbit = pixelbits[pixelformat] - local bytesRemaining = 32768 - local datastr = {} - - local range = self.ChangedCellRanges[self.ChangedStep] - local sTime = SysTime() - while range and bytesRemaining>0 and SysTime() - sTime < maxProcessingTime do - bytesRemaining = buildData(datastr, self.Memory, pixelbit, range, bytesRemaining, sTime) - if range.length==0 then - self.ChangedStep = self.ChangedStep + 1 - range = self.ChangedCellRanges[self.ChangedStep] - end - end - - local n = #self.ChangedCellRanges - - self.deltaStep = (self.deltaStep or 0) * 0.5 + self.ChangedStep * 0.5 - self.deltaN = (self.deltaN or 0) * 0.5 + n * 0.5 - - if - -- reset queue if we've reached the end - self.ChangedStep > n or - - -- if the queue length keeps growing faster than we can process it, just clear it - -- this check is mostly to detect the worst possible case where the user spams single random pixels - (n > self.ScreenWidth * self.ScreenHeight and self.deltaStep * 4 < self.deltaN) then - - self.ChangedCellRanges = {} - self.ChangedStep = 1 - end - - numberToString(datastr,0,3) - datastr = util.Compress(table.concat(datastr)) - local len = #datastr - - net.Start("wire_digitalscreen") - net.WriteUInt(self:EntIndex(),16) - net.WriteUInt(pixelformat, 5) - net.WriteUInt(len, 32) - net.WriteData(datastr,len) - - addGlobalBW(self) - self.UpdateRate = math.Round(math.max(len / maxBandwidth, 0.05),2) - - if ply then net.Send(ply) else net.Broadcast() end -end - -function ENT:Retransmit(ply) - self:FlushCache() -- Empty the cache - - self:MarkCellChanged(1048569) -- Colormode - self:MarkCellChanged(1048572) -- Screen Width - self:MarkCellChanged(1048573) -- Screen Height - self:MarkCellChanged(1048575) -- Clk - self:FlushCache(ply) - - local memory = self.Memory - for addr=0, self.ScreenWidth*self.ScreenHeight do - if memory[addr] then - self:MarkCellChanged(addr) - end - end - self:MarkCellChanged(1048575) -- Clk - self:FlushCache(ply) -end - -function ENT:ClearPixel(i) - if self.Memory[1048569] == 1 then - -- R G B mode - i = i * 3 - self.Memory[i] = 0 - self.Memory[i+1] = 0 - self.Memory[i+2] = 0 - self:MarkCellChanged(i) - self:MarkCellChanged(i+1) - self:MarkCellChanged(i+2) - return - end - - -- other modes - self.Memory[i] = 0 - self:MarkCellChanged(i) -end - -function ENT:ClearCellRange(start, length) - for i = start, start + length - 1 do - self.Memory[i] = 0 - self:MarkCellChanged(i) - end -end - -function ENT:WriteCell(Address, value) - Address = math.floor (Address) - value = math.floor(value or 0) - if Address < 0 then return false end - if Address >= 1048577 then return false end - - if Address < 1048500 then -- RGB data - if self.Memory[Address] == value or - (value == 0 and self.Memory[Address] == nil) then - return true - end - else - if Address == 1048569 then - -- Color mode (0: RGBXXX; 1: R G B; 2: 24 bit RGB; 3: RRRGGGBBB; 4: XXX) - value = math.Clamp(value, 0, 9) - elseif Address == 1048570 then -- Clear row - local row = math.Clamp(value, 0, self.ScreenHeight-1) - if self.Memory[1048569] == 1 then - self:ClearCellRange(row*self.ScreenWidth*3, self.ScreenWidth*3) - else - self:ClearCellRange(row*self.ScreenWidth, self.ScreenWidth) - end - elseif Address == 1048571 then -- Clear column - local col = math.Clamp(value, 0, self.ScreenWidth-1) - for i = col,col+self.ScreenWidth*(self.ScreenHeight-1),self.ScreenWidth do - self:ClearPixel(i) - end - elseif Address == 1048572 then -- Height - self.ScreenHeight = math.Clamp(value, 1, 512) - elseif Address == 1048573 then -- Width - self.ScreenWidth = math.Clamp(value, 1, 512) - elseif Address == 1048574 then -- Hardware Clear Screen - - -- delete changed cells - self.ChangedCellRanges = {} - self.ChangedStep = 1 - - -- copy every value above pixel data - local mem = {} - for addr = 1048500,1048575 do - mem[addr] = self.Memory[addr] - - if self.Memory[addr] then - -- re-mark cell changed - self:MarkCellChanged(addr) - end - end - - -- reset memory - self.Memory = mem - -- elseif Address == 1048575 then -- CLK - -- not needed atm - end - end - - self.Memory[Address] = value - - self:MarkCellChanged(Address) - - return true -end - -function ENT:Think() - self:FlushCache() - self:NextThink(CurTime()+self.UpdateRate) - return true -end - -function ENT:TriggerInput(iname, value) - if (iname == "PixelX") then - self.PixelX = math.floor(value) - self:SendPixel() - elseif (iname == "PixelY") then - self.PixelY = math.floor(value) - self:SendPixel() - elseif (iname == "PixelG") then - self.PixelG = math.floor(value) - self:SendPixel() - elseif (iname == "Clk") then - self:WriteCell(1048575, value) - self:SendPixel() - elseif (iname == "FillColor") then - self:WriteCell(1048574,value) - elseif (iname == "ClearCol") then - self:WriteCell(1048571,math.Clamp( value, 0, 31 )) - elseif (iname == "ClearRow") then - self:WriteCell(1048570,math.Clamp( value, 0, 31 )) - end -end - -duplicator.RegisterEntityClass("gmod_wire_digitalscreen", WireLib.MakeWireEnt, "Data", "ScreenWidth", "ScreenHeight", "IsInteractive") +AddCSLuaFile( "cl_init.lua" ) +AddCSLuaFile( "shared.lua" ) +include("shared.lua") +DEFINE_BASECLASS( "base_wire_entity" ) + +ENT.WireDebugName = "DigitalScreen" + +local dsRate = CreateConVar("wire_digitalscreen_rate", 1, { FCVAR_REPLICATED, FCVAR_ARCHIVE }) + +function ENT:InitInteractive() + local model = self:GetModel() + local outputs = {"Memory"} + local interactivemodel = WireLib.GetInteractiveModel(model) + for i=1, #interactivemodel.widgets do + outputs[i+1] = interactivemodel.widgets[i].name + end + self.BlockInput = false + self.NextPrompt = 0 + self.Outputs=WireLib.CreateOutputs(self,outputs) + self.IsInteractive = true + self:UpdateOverlay() +end + + +function ENT:ReceiveData() + if not self.IsInteractive then return end + local data = WireLib.GetInteractiveModel(self:GetModel()).widgets + for i = 1, #data do + WireLib.TriggerOutput(self, data[i].name, net.ReadFloat()) + end +end + +function ENT:UpdateOverlay() + if not self.IsInteractive then + return + end + + txt = "" + if IsValid(self.User) then + txt = "In use by: " .. self.User:Nick() + end + + self:SetOverlayText(txt) +end + + +function ENT:Prompt( ply ) + if not self.IsInteractive then return end + if ply then + if CurTime() < self.NextPrompt then return end -- anti spam + self.NextPrompt = CurTime() + 0.1 + + if IsValid( self.User ) then + WireLib.AddNotify(ply,"That interactive prop is in use by another player!",NOTIFY_ERROR,5,6) + return + end + + self.User = ply + + net.Start( "wire_interactiveprop_show" ) + net.WriteEntity( self ) + net.Send( ply ) + else + self:Prompt( self:GetPlayer() ) -- prompt for owner + end +end + +function ENT:Use(ply) + if not IsValid( ply ) then return end + self:Prompt( ply ) +end + +function ENT:Unprompt() + if not self.IsInteractive then return end + self.User = nil +end + +function ENT:Initialize() + + self:PhysicsInit(SOLID_VPHYSICS) + self:SetMoveType(MOVETYPE_VPHYSICS) + self:SetSolid(SOLID_VPHYSICS) + + self.Inputs = Wire_CreateInputs(self, { "PixelX", "PixelY", "PixelG", "Clk", "FillColor", "ClearRow", "ClearCol" }) + if WireLib.IsValidInteractiveModel(self:GetModel()) then + self:InitInteractive() + else + self.Outputs = WireLib.CreateOutputs(self, { "Memory" }) + end + + self.Memory = {} + + self.PixelX = 0 + self.PixelY = 0 + self.PixelG = 0 + self.Memory[1048569] = 0 + self.Memory[1048572] = 32 + self.Memory[1048573] = 32 + self.Memory[1048575] = 1 + + self.ScreenWidth = 32 + self.ScreenHeight = 32 + + self.NumOfWrites = 0 + self.UpdateRate = 0.1 + + self.ChangedCellRanges = {} + self.ChangedStep = 1 +end + +function ENT:Setup(ScreenWidth, ScreenHeight, IsInteractive) + self:WriteCell(1048572, ScreenHeight or 32) + self:WriteCell(1048573, ScreenWidth or 32) + self.IsInteractive = WireLib.IsValidInteractiveModel(self:GetModel()) and (IsInteractive == 1) +end + +function ENT:SendPixel() + if self.Memory[1048575] == 0 then return end -- why? + if self.PixelX < 0 then return end + if self.PixelY < 0 then return end + if self.PixelX >= self.ScreenWidth then return end + if self.PixelY >= self.ScreenHeight then return end + + local address = self.PixelY*self.ScreenWidth + self.PixelX + self:WriteCell(address, self.PixelG) +end + +function ENT:ReadCell(Address) + Address = math.floor(Address) + if Address < 0 then return nil end + if Address >= 1048577 then return nil end + + return self.Memory[Address] or 0 +end + +function ENT:MarkCellChanged(Address) + self.NumOfWrites = self.NumOfWrites + 1 + + local lastrange = self.ChangedCellRanges[#self.ChangedCellRanges] + if lastrange then + if Address == lastrange.start + lastrange.length then + -- wrote just after the end of the range, append + lastrange.length = lastrange.length + 1 + elseif Address == lastrange.start - 1 then + -- wrote just before the start of the range, prepend + lastrange.start = lastrange.start - 1 + lastrange.length = lastrange.length + 1 + elseif Address < lastrange.start - 1 or Address > lastrange.start + lastrange.length then + -- wrote outside the range + lastrange = nil + end + end + if not lastrange then + lastrange = { + start = Address, + length = 1 + } + self.ChangedCellRanges[#self.ChangedCellRanges + 1] = lastrange + end +end + +local function numberToString(t, number, bytes) + local str = {} + for j=1,bytes do + str[j] = string.char(number % 256) + number = math.floor(number / 256) + end + t[#t+1] = table.concat(str) +end + +---------------------------------------------------- +-- Processing limiters and global bandwidth limiters +local maxProcessingTime = engine.TickInterval() * 0.9 + +local dsRateValue = dsRate:GetFloat() +local defaultMaxBandwidth = 100000 * dsRateValue -- 10k per screen max limit - is arbitrary. needs to be smaller than the global limit. +local defaultMaxGlobalBandwidth = 200000 * dsRateValue -- 20k is a good global limit in my testing. higher than that seems to cause issues +local maxBandwidth = defaultMaxBandwidth + +local function updateBW() + local dsRateValue = dsRate:GetFloat() + + defaultMaxBandwidth = 100000 * dsRateValue + defaultMaxGlobalBandwidth = 200000 * dsRateValue +end +cvars.AddChangeCallback("wire_digitalscreen_rate", updateBW) + +local globalBandwidthLookup = {} +local function calcGlobalBW() + maxBandwidth = defaultMaxGlobalBandwidth + local n = 0 + + -- count the number of digi screens currently sending data + for digi in pairs(globalBandwidthLookup) do + if not IsValid(digi) then globalBandwidthLookup[digi] = nil end -- this most likely won't trigger due to OnRemove, but just in case + n = n + 1 + end + + -- player count also seems to affect lag somewhat + -- it seems logical that this would have something to do with the upload bandwidth of the server + -- but that seems unlikely since testing shows that the amount of data sent isn't very high + -- it's more likely that the net message library just isn't very efficient + -- the numbers here are picked somewhat arbitrarily, with a bit of guessing. + -- change in the future if necessary. + n = n + math.max(0,player.GetCount()-2) / 4 + + -- during testing, lag seems to increase somewhat faster as more net messages are sent at the same time + -- so we double this value to compensate + n = n * 2 + + maxBandwidth = math.max(100,math.Round(math.min(defaultMaxBandwidth,maxBandwidth / n),2)) +end +local function addGlobalBW(e) + globalBandwidthLookup[e] = true + calcGlobalBW() +end +local function removeGlobalBW(e) globalBandwidthLookup[e] = nil end +---------------------------------------------------- + +function ENT:OnRemove() + BaseClass.OnRemove(self) + removeGlobalBW(self) +end + +local function buildData(datastr, memory, pixelbit, range, bytesRemaining, sTime) + if bytesRemaining < 15 then return 0 end + local lengthIndex = #datastr+1 + datastr[lengthIndex] = "000" + numberToString(datastr,range.start,3) -- Address of range + bytesRemaining = bytesRemaining - 6 + local i, iend = range.start, range.start + range.length + while i0 and SysTime() - sTime < maxProcessingTime do + if i>=1048500 then + numberToString(datastr,memory[i],2) + bytesRemaining = bytesRemaining - 2 + else + numberToString(datastr,memory[i],pixelbit) + bytesRemaining = bytesRemaining - pixelbit + end + i = i + 1 + end + local lengthStr = {} + numberToString(lengthStr,i - range.start,3) -- Length of range + datastr[lengthIndex] = lengthStr[1] + range.length = iend - i + range.start = i + + return bytesRemaining +end + +util.AddNetworkString("wire_digitalscreen") + + +local pixelbits = {3, 1, 3, 4, 1} --The compressed pixel formats are in bytes +function ENT:FlushCache(ply) + if not next(self.ChangedCellRanges) then + removeGlobalBW(self) + return + end + + local pixelformat = (math.floor(self.Memory[1048569]) or 0) + 1 + if pixelformat < 1 or pixelformat > #pixelbits then pixelformat = 1 end + local pixelbit = pixelbits[pixelformat] + local bytesRemaining = 32768 + local datastr = {} + + local range = self.ChangedCellRanges[self.ChangedStep] + local sTime = SysTime() + while range and bytesRemaining>0 and SysTime() - sTime < maxProcessingTime do + bytesRemaining = buildData(datastr, self.Memory, pixelbit, range, bytesRemaining, sTime) + if range.length==0 then + self.ChangedStep = self.ChangedStep + 1 + range = self.ChangedCellRanges[self.ChangedStep] + end + end + + local n = #self.ChangedCellRanges + + self.deltaStep = (self.deltaStep or 0) * 0.5 + self.ChangedStep * 0.5 + self.deltaN = (self.deltaN or 0) * 0.5 + n * 0.5 + + if + -- reset queue if we've reached the end + self.ChangedStep > n or + + -- if the queue length keeps growing faster than we can process it, just clear it + -- this check is mostly to detect the worst possible case where the user spams single random pixels + (n > self.ScreenWidth * self.ScreenHeight and self.deltaStep * 4 < self.deltaN) then + + self.ChangedCellRanges = {} + self.ChangedStep = 1 + end + + numberToString(datastr,0,3) + datastr = util.Compress(table.concat(datastr)) + local len = #datastr + + net.Start("wire_digitalscreen") + net.WriteUInt(self:EntIndex(),16) + net.WriteUInt(pixelformat, 5) + net.WriteUInt(len, 32) + net.WriteData(datastr,len) + + addGlobalBW(self) + self.UpdateRate = math.Round(math.max(len / maxBandwidth, 0.05),2) + + if ply then net.Send(ply) else net.Broadcast() end +end + +function ENT:Retransmit(ply) + self:FlushCache() -- Empty the cache + + self:MarkCellChanged(1048569) -- Colormode + self:MarkCellChanged(1048572) -- Screen Width + self:MarkCellChanged(1048573) -- Screen Height + self:MarkCellChanged(1048575) -- Clk + self:FlushCache(ply) + + local memory = self.Memory + for addr=0, self.ScreenWidth*self.ScreenHeight do + if memory[addr] then + self:MarkCellChanged(addr) + end + end + self:MarkCellChanged(1048575) -- Clk + self:FlushCache(ply) +end + +function ENT:ClearPixel(i) + if self.Memory[1048569] == 1 then + -- R G B mode + i = i * 3 + self.Memory[i] = 0 + self.Memory[i+1] = 0 + self.Memory[i+2] = 0 + self:MarkCellChanged(i) + self:MarkCellChanged(i+1) + self:MarkCellChanged(i+2) + return + end + + -- other modes + self.Memory[i] = 0 + self:MarkCellChanged(i) +end + +function ENT:ClearCellRange(start, length) + for i = start, start + length - 1 do + self.Memory[i] = 0 + self:MarkCellChanged(i) + end +end + +function ENT:WriteCell(Address, value) + Address = math.floor (Address) + value = math.floor(value or 0) + if Address < 0 then return false end + if Address >= 1048577 then return false end + + if Address < 1048500 then -- RGB data + if self.Memory[Address] == value or + (value == 0 and self.Memory[Address] == nil) then + return true + end + else + if Address == 1048569 then + -- Color mode (0: RGBXXX; 1: R G B; 2: 24 bit RGB; 3: RRRGGGBBB; 4: XXX) + value = math.Clamp(value, 0, 9) + elseif Address == 1048570 then -- Clear row + local row = math.Clamp(value, 0, self.ScreenHeight-1) + if self.Memory[1048569] == 1 then + self:ClearCellRange(row*self.ScreenWidth*3, self.ScreenWidth*3) + else + self:ClearCellRange(row*self.ScreenWidth, self.ScreenWidth) + end + elseif Address == 1048571 then -- Clear column + local col = math.Clamp(value, 0, self.ScreenWidth-1) + for i = col,col+self.ScreenWidth*(self.ScreenHeight-1),self.ScreenWidth do + self:ClearPixel(i) + end + elseif Address == 1048572 then -- Height + self.ScreenHeight = math.Clamp(value, 1, 512) + elseif Address == 1048573 then -- Width + self.ScreenWidth = math.Clamp(value, 1, 512) + elseif Address == 1048574 then -- Hardware Clear Screen + + -- delete changed cells + self.ChangedCellRanges = {} + self.ChangedStep = 1 + + -- copy every value above pixel data + local mem = {} + for addr = 1048500,1048575 do + mem[addr] = self.Memory[addr] + + if self.Memory[addr] then + -- re-mark cell changed + self:MarkCellChanged(addr) + end + end + + -- reset memory + self.Memory = mem + -- elseif Address == 1048575 then -- CLK + -- not needed atm + end + end + + self.Memory[Address] = value + + self:MarkCellChanged(Address) + + return true +end + +function ENT:Think() + self:FlushCache() + self:NextThink(CurTime()+self.UpdateRate) + return true +end + +function ENT:TriggerInput(iname, value) + if (iname == "PixelX") then + self.PixelX = math.floor(value) + self:SendPixel() + elseif (iname == "PixelY") then + self.PixelY = math.floor(value) + self:SendPixel() + elseif (iname == "PixelG") then + self.PixelG = math.floor(value) + self:SendPixel() + elseif (iname == "Clk") then + self:WriteCell(1048575, value) + self:SendPixel() + elseif (iname == "FillColor") then + self:WriteCell(1048574,value) + elseif (iname == "ClearCol") then + self:WriteCell(1048571,math.Clamp( value, 0, 31 )) + elseif (iname == "ClearRow") then + self:WriteCell(1048570,math.Clamp( value, 0, 31 )) + end +end + +duplicator.RegisterEntityClass("gmod_wire_digitalscreen", WireLib.MakeWireEnt, "Data", "ScreenWidth", "ScreenHeight", "IsInteractive")