-- Color only the captured latency widget. Signal sprites contain all bars in -- one atlas image: three UV slices preserve its alpha without replacing art. do local CS=CS local U=CS.UnityEngine local UI=U.UI local VERSION=7 local SPEED,SPACING=0.16,0.13 local PATHS={ 'UIRoot/Page/MazeMainPage(Clone)/RightTopArea/NetStatusPanel/NetStatusPanel(Clone)', 'UIRoot/Page/BattleGamePhaseUI(Clone)/FunctionArea/NetStatusPanel/NetStatusPanel', } local old=_G.__GAC_PING_RAINBOW_STATE local function event(target,name,op,fn) return pcall(function()target[name](op,fn)end) end if old and old.version~=VERSION then if old.callback then -- v5 tried both event binding forms; remove both possible subscriptions. for _,entry in ipairs({{U.Camera,'onPreRender'},{U.Camera,'onPreCull'},{U.Application,'onBeforeRender'},{U.Canvas,'willRenderCanvases'}})do event(entry[1],entry[2],'-',old.callback);event(entry[1],entry[2],'-',old.callback) end end if old.dispose then pcall(old.dispose)end old=nil end local s=old or {version=VERSION,targets={},nextScan=0,lastFrame=-1,nextColor=0} _G.__GAC_PING_RAINBOW_STATE=s local function log(message)U.Debug.Log('[GaC Ping Colors] '..message)end local function color(h,a) local v=(h%1)*6;local x=1-math.abs(v%2-1) if v<1 then return U.Color(1,x,0,a) elseif v<2 then return U.Color(x,1,0,a) elseif v<3 then return U.Color(0,1,x,a) elseif v<4 then return U.Color(0,x,1,a) elseif v<5 then return U.Color(x,0,1,a) else return U.Color(1,0,x,a)end end local function dispose_target(target) for _,icon in ipairs(target.icons or {})do for _,slice in ipairs(icon.slices or {})do pcall(function()U.Object.Destroy(slice.gameObject)end)end if icon.mask then pcall(function()U.Object.Destroy(icon.mask)end)end pcall(function()icon.image.enabled=icon.enabled;icon.image.color=icon.color end) end if target.text then pcall(function() if target.text.text==target.rendered then target.text.text=target.base or ''end target.text.color=target.textColor;target.text.supportRichText=target.rich end)end if target.mesh then pcall(function()U.Object.Destroy(target.mesh)end)end end s.dispose=function()for _,target in pairs(s.targets)do dispose_target(target)end;s.targets={}end local function white_mask(sprite) local r=sprite.textureRect local width,height=math.floor(r.width),math.floor(r.height) if width<=0 or height<=0 or width>128 or height>128 then error('signal mask outside small-widget bound')end local texture,temporary local previous=U.RenderTexture.active local ok,err=pcall(function() local source=sprite.texture temporary=U.RenderTexture.GetTemporary(width,height,0,U.RenderTextureFormat.ARGB32) U.Graphics.Blit(source,temporary,U.Vector2(r.width/source.width,r.height/source.height),U.Vector2(r.x/source.width,r.y/source.height)) U.RenderTexture.active=temporary texture=U.Texture2D(width,height,U.TextureFormat.RGBA32,false) texture:ReadPixels(U.Rect(0,0,width,height),0,0) local pixels=texture:GetPixels32() for i=0,pixels.Length-1 do pixels[i]=U.Color32(255,255,255,pixels[i].a)end texture:SetPixels32(pixels);texture:Apply(false,true) texture.name='GaCPingAlphaMask';texture.filterMode=U.FilterMode.Bilinear texture.wrapMode=U.TextureWrapMode.Clamp end) U.RenderTexture.active=previous if temporary then U.RenderTexture.ReleaseTemporary(temporary)end if not ok then if texture then U.Object.Destroy(texture)end;error(err)end return texture end local function make_slices(icon) local sprite=icon.image.sprite if not sprite then error('signal sprite unavailable')end if sprite.packingRotation~=U.SpritePackingRotation.None then error('rotated signal atlas not supported')end local rect=sprite.textureRect if sprite.textureRectOffset.x~=0 or sprite.textureRectOffset.y~=0 or rect.width~=sprite.rect.width or rect.height~=sprite.rect.height then error('trimmed signal sprite not supported')end icon.mask=white_mask(sprite) for i=0,2 do local go=U.GameObject('GaCPingColorSlice'..tostring(i),typeof(U.RectTransform),typeof(UI.RawImage)) local rt=go:GetComponent(typeof(U.RectTransform)) -- Track immediately so a later binding failure cannot leave an overlay. local raw=go:GetComponent(typeof(UI.RawImage));icon.slices[#icon.slices+1]=raw rt:SetParent(icon.image.transform,false) rt.anchorMin=U.Vector2(i/3,0);rt.anchorMax=U.Vector2((i+1)/3,1) rt.offsetMin=U.Vector2.zero;rt.offsetMax=U.Vector2.zero raw.raycastTarget=false;raw.texture=icon.mask raw.uvRect=U.Rect(i/3,0,1/3,1) end icon.image.enabled=false end local function discover(path) local root=U.GameObject.Find(path) if not root then return end local target={root=root,id=root:GetInstanceID(),icons={}} local ok,err=pcall(function() local connected=root.transform:Find('Connected') if not connected then error('Connected node unavailable')end local time=connected:Find('Time');target.text=time and time:GetComponent(typeof(UI.Text)) if not target.text then error('latency Text unavailable')end target.textColor=target.text.color;target.rich=target.text.supportRichText target.mesh=U.Mesh();target.mesh.name='GaCPingLatencyColors' for _,name in ipairs({'Level1','Level2','Level3'})do local t=connected:Find(name);local img=t and t:GetComponent(typeof(UI.Image)) if img then target.icons[#target.icons+1]={image=img,enabled=img.enabled,color=img.color,slices={}}end end if #target.icons==0 then error('signal level Images unavailable')end end) if not ok then dispose_target(target);error(err)end log('READY '..path..' icons='..tostring(#target.icons)..' mode=white-alpha-slices/text-vertex-colors real-latency=true') return target end _G.__GAC_PING_RAINBOW_TICK=function() local frame=U.Time.frameCount if s.lastFrame==frame then return end;s.lastFrame=frame local now=U.Time.unscaledTime for _,path in ipairs(PATHS)do local ok,err=pcall(function() local target=s.targets[path] if target then local alive=pcall(function()return target.root.transform.childCount end) if not alive then dispose_target(target);s.targets[path]=nil;target=nil end end if not target and now>=s.nextScan then target=discover(path);s.targets[path]=target end if not target or not target.root.activeInHierarchy then return end local phase=(now*SPEED)%1 for _,icon in ipairs(target.icons)do if icon.image.gameObject.activeInHierarchy then if #icon.slices==0 then make_slices(icon)end for i,slice in ipairs(icon.slices)do slice.color=color(phase+(i-1)*SPACING,icon.color.a)end end end -- Color the freshly generated native glyph mesh at the canvas boundary. -- Never rewrite latency text: number changes cannot expose an untagged -- cyan/white frame between a native setter and a rich-text repaint. local text=target.text;target.base=tostring(text.text or '') -- Unity 2019 exposes SetMesh but no CanvasRenderer.GetMesh. Its Text -- generator retains the same glyph quads used by Text.OnPopulateMesh. -- Captured here: "10ms" has 16 vertices, all four quads populated; this -- engine does not append the unused quad found in older uGUI sources. local verts=text.cachedTextGenerator.verts local count=verts.Count if count>0 and count<=512 and count%4==0 then if target.vertexCount~=count then target.mesh:Clear() target.vertexCount=count target.positions=CS.System.Array.CreateInstance(typeof(U.Vector3),count) target.uvs=CS.System.Array.CreateInstance(typeof(U.Vector2),count) target.colors=CS.System.Array.CreateInstance(typeof(U.Color32),count) target.indices=CS.System.Array.CreateInstance(typeof(CS.System.Int32),count/4*6) for q=0,count/4-1 do local v=q*4;local t=q*6 target.indices[t]=v;target.indices[t+1]=v+1;target.indices[t+2]=v+2 target.indices[t+3]=v+2;target.indices[t+4]=v+3;target.indices[t+5]=v end end local scale=1/text.pixelsPerUnit local offset=text:PixelAdjustPoint(U.Vector2.zero) for i=0,count-1 do local vertex=verts[i];local pos=vertex.position target.positions[i]=U.Vector3(pos.x*scale+offset.x,pos.y*scale+offset.y,pos.z*scale) target.uvs[i]=U.Vector2(vertex.uv0.x,vertex.uv0.y) local c=color(phase+(3+math.floor(i/4))*SPACING,1) target.colors[i]=U.Color32(math.floor(c.r*255+0.5),math.floor(c.g*255+0.5),math.floor(c.b*255+0.5),vertex.color.a) end target.mesh.vertices=target.positions;target.mesh.uv=target.uvs target.mesh.colors32=target.colors;target.mesh.triangles=target.indices text.canvasRenderer:SetMesh(target.mesh) end end) if not ok then if s.targets[path]then dispose_target(s.targets[path]);s.targets[path]=nil end if s.error~=tostring(err)then s.error=tostring(err);log('ERROR '..s.error)end end end if now>=s.nextScan then s.nextScan=now+1 end end if not s.callback then s.callback=function()if _G.__GAC_PING_RAINBOW_TICK then _G.__GAC_PING_RAINBOW_TICK()end end s.registered=event(U.Canvas,'willRenderCanvases','+',s.callback) if not s.registered then s.registered=event(U.Application,'onBeforeRender','+',s.callback)end log('CALLBACK registered='..tostring(s.registered)) end -- The heartbeat only installs/replaces the callback. A heartbeat-time paint -- must not suppress the later canvas paint for that same frame. end