Files
GaCy-GO/scripts/ping_rainbow.lua
T

190 lines
9.2 KiB
Lua
Raw Normal View History

2026-10-28 18:54:43 +07:00
-- 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