191 lines
9.2 KiB
Lua
191 lines
9.2 KiB
Lua
-- 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
|