Magnifier inspection view for graphics cards

Cards are inspectable. While the magnifier inspection tray shows a card, a
mod-owned panel under the tray draws the card enlarged with its appearance
overlays and a turn-over button. No Harmony patches: a resident component
watches the inspection manager.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 12:08:51 +08:00
1 parent e643332a41
commit c05fe41ca9
6 files changed
+169 -17

No files matched your search

+15
View File
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Decentralized.Domain;
namespace Decentralized
@@ -23,6 +24,20 @@ namespace Decentralized
Items.Give(Items.NewCard("RZ6800", 30, "RZ580", rng));
Items.Give(Items.NewCard("RZ7900", 90, "RZ470", rng));
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dump inspection UI (test)", () => Log.Guard("检视界面结构", () =>
{
var sb = new System.Text.StringBuilder("\n");
void Walk(Transform t, int depth)
{
var rt = t as RectTransform;
var img = t.GetComponent<UnityEngine.UI.Image>();
sb.AppendLine($"{new string(' ', depth * 2)}{t.name} active={t.gameObject.activeSelf} pos={t.localPosition} size={(rt != null ? rt.sizeDelta.ToString() : "-")} scale={t.localScale}{(img != null ? " img=" + (img.sprite != null ? img.sprite.name : "null") : "")}");
if (depth < 5) foreach (Transform c in t) Walk(c, depth + 1);
}
var m = InspectionUIManager.Instance;
Walk(m.panel.transform, 0);
Log.Info(sb.ToString());
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
{
EmporiumEntry.Instance.ClearFrontInv();
+113
View File
@@ -0,0 +1,113 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Decentralized
{
/// <summary>
/// 放大镜检视显卡时的大图:和香烟一样把物品放大摆在检视台上,正反面各一张,看外观自己判断真假。
/// 游戏没有给 mod 开放检视界面,这里不打补丁,只在检视台(InspectionUIManager.panel)下面挂一个自己的界面,
/// 用一个常驻的 MonoBehaviour 每帧看一眼是否正在检视显卡。
/// </summary>
public sealed class GpuInspectView : MonoBehaviour
{
private static GpuInspectView instance;
private RectTransform root, cardRoot;
private Button flip;
private TextMeshProUGUI flipText;
private GameItem shown;
private string shownSig;
public static void Ensure()
{
if (instance != null) return;
var go = new GameObject("pd_gpu_inspect_view");
DontDestroyOnLoad(go);
instance = go.AddComponent<GpuInspectView>();
}
public static void Remove()
{
if (instance != null) Destroy(instance.gameObject);
instance = null;
}
private void Update()
{
var m = InspectionUIManager.Instance;
var item = m != null && m.panel != null && m.panel.activeSelf ? m.currentInspectingItem : null;
if (item == null || !Items.IsGpu(item))
{
if (root != null && root.gameObject.activeSelf) root.gameObject.SetActive(false);
shown = null;
return;
}
Log.Guard("显卡检视大图", () =>
{
if (root == null) Build(m);
if (!root.gameObject.activeSelf) root.gameObject.SetActive(true);
var keys = new List<string>();
string sig = Items.LookKeys(item, keys) + "|" + string.Join(",", keys);
if (item != shown || sig != shownSig)
{
shown = item;
shownSig = sig;
Redraw(item);
}
});
}
private void Build(InspectionUIManager m)
{
root = new GameObject("_PdGpuInspection", typeof(RectTransform)).GetComponent<RectTransform>();
root.SetParent(m.panel.transform, false);
root.sizeDelta = new Vector2(1920, 1080);
root.localPosition = Vector3.zero;
cardRoot = new GameObject("Card", typeof(RectTransform)).GetComponent<RectTransform>();
cardRoot.SetParent(root, false);
cardRoot.localPosition = new Vector3(0, 40, 0);
// 翻面按钮:复制鉴定面板里"发送"按钮的外观
var template = m.appraisalPanel.transform.Find("buttonSend");
if (template != null)
{
var go = Instantiate(template.gameObject, root, false);
go.name = "Flip";
go.transform.localPosition = new Vector3(0, -300, 0);
flip = go.GetComponent<Button>();
flip.onClick = new Button.ButtonClickedEvent();
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
flipText = go.GetComponentInChildren<TextMeshProUGUI>();
// 去掉复制来的按钮上驱动文字的组件(本地化 / 提交逻辑),文字由这里设置
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
string n = mb.GetType().Name;
if (mb is Button || mb is Image || mb is TextMeshProUGUI || mb is CanvasRenderer) continue;
if (n.Contains("Local") || n.Contains("Appraisal") || n.Contains("Send")) Destroy(mb);
}
}
}
private void Redraw(GameItem item)
{
foreach (Transform c in cardRoot) Destroy(c.gameObject);
var keys = new List<string>();
string baseKey = Items.LookKeys(item, keys);
keys.Insert(0, baseKey);
foreach (var k in keys)
{
var sprite = Items.ModSprite(k);
if (sprite == null) continue;
var go = new GameObject(k, typeof(RectTransform), typeof(Image));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(cardRoot, false);
rt.sizeDelta = new Vector2(sprite.rect.width, sprite.rect.height);
rt.localScale = Vector3.one * 14f;
var img = go.GetComponent<Image>();
img.sprite = sprite;
img.raycastTarget = false;
}
if (flipText != null) flipText.text = L.T(Items.IsBack(item) ? "inspect.flip.front" : "inspect.flip.back");
}
}
}
+35 -17
View File
@@ -70,6 +70,7 @@ namespace Decentralized
{
var item = ItemDirectory.CreateEmptyItem(GpuId(spec.Model));
item.SetGameItemType("MATERIAL");
InspectableHelper.InitInspectableItem(item);
item.onLoaded = Refresh;
// 右键"切换"翻面;外观差异用叠加图画在同一张底图上
item.EnableTag(ToggleAppearanceHelper.TOGGLE_APPEARANCE_TAG);
@@ -77,8 +78,7 @@ namespace Decentralized
item.canToggleSlotItemFunc = (_, _, _) => true;
item.onToggleSlotItemFunc = (toggled, _, _) => Log.Guard("显卡翻面", () =>
{
if (toggled.IsTag(BackTag)) toggled.ModifyTag(BackTag, t => t.Disable()); else toggled.EnableTag(BackTag);
Refresh(toggled);
Flip(toggled);
});
item.getItemSpriteModifierForItemFunc = Overlays;
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
@@ -152,33 +152,51 @@ namespace Decentralized
SyncValue(item);
}
private static Sprite ModSprite(string key)
public static bool IsBack(GameItem item) => item.IsTag(BackTag);
public static void Flip(GameItem item)
{
var mod = ModLoader.Mods.Find(m => m.Manifest.ID.Equals(ModInfo.Id));
return mod != null && mod.Sprites.TryGetValue(key, out var sprite) ? sprite : null;
if (IsBack(item)) item.ModifyTag(BackTag, t => t.Disable()); else item.EnableTag(BackTag);
Refresh(item);
}
/// <summary>外观叠加图:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
private static List<ItemSpriteModifier> Overlays(GameItem item)
/// <summary>当前面的底图名与叠加图名:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
public static string LookKeys(GameItem item, List<string> overlays)
{
var list = new List<ItemSpriteModifier>();
SyncValue(item); // 归属变化(买下)后价格随之变回真实价值
if (item.IsTag(DeadTag)) return list;
var u = ToUnit(item);
var keys = new List<string>();
if (!item.IsTag(BackTag))
bool back = IsBack(item);
string brand = u.Label.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd";
if (u.Dead) return back ? "gpu_burnt_b" : "gpu_burnt_f";
if (!back)
{
if (u.Look.Fan > 0) keys.Add("gpu_ov_fan" + u.Look.Fan);
if (u.Look.Fingers > 0) keys.Add("gpu_ov_fin" + u.Look.Fingers);
if (u.Look.Bracket > 0) keys.Add("gpu_ov_brk" + u.Look.Bracket);
if (u.Look.Fan > 0) overlays.Add("gpu_ov_fan" + u.Look.Fan);
if (u.Look.Fingers > 0) overlays.Add("gpu_ov_fin" + u.Look.Fingers);
if (u.Look.Bracket > 0) overlays.Add("gpu_ov_brk" + u.Look.Bracket);
}
else
{
// 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过)
string model = u.Look.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model;
keys.Add((u.Look.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
if (u.Look.Sticker > 0) keys.Add("gpu_ov_stk" + u.Look.Sticker);
overlays.Add((u.Look.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
if (u.Look.Sticker > 0) overlays.Add("gpu_ov_stk" + u.Look.Sticker);
}
return brand + (back ? "_b" : "_f");
}
public static Sprite ModSprite(string key)
{
var mod = ModLoader.Mods.Find(m => m.Manifest.ID.Equals(ModInfo.Id));
return mod != null && mod.Sprites.TryGetValue(key, out var sprite) ? sprite : null;
}
/// <summary>物品栏里的外观叠加图。</summary>
private static List<ItemSpriteModifier> Overlays(GameItem item)
{
var list = new List<ItemSpriteModifier>();
SyncValue(item); // 归属变化(买下)后价格随之变回真实价值
if (item.IsTag(DeadTag)) return list;
var keys = new List<string>();
LookKeys(item, keys);
int priority = 0;
foreach (var k in keys)
{
+2
View File
@@ -16,6 +16,7 @@ namespace Decentralized
Night.Subscribe();
DealerStock.Subscribe();
UsedDealerClient.Subscribe();
GpuInspectView.Ensure();
ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame;
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
@@ -27,6 +28,7 @@ namespace Decentralized
Night.Unsubscribe();
DealerStock.Unsubscribe();
UsedDealerClient.Unsubscribe();
GpuInspectView.Remove();
ModHook.OnGameLoadedLate -= Session.Load;
ModHook.OnNewGameStarted -= OnNewGame;
}
@@ -66,3 +66,5 @@ dealer.line1.3,"Picked these up on the road. First-hand price."
dealer.line2.1,"See for yourself, turn them over and check the back."
dealer.line2.2,"No warranty. Look before you take."
dealer.line2.3,"Price is negotiable if you want them."
inspect.flip.back,"Turn over"
inspect.flip.front,"Front side"
1 # Probably Decentralized localization table: English (en)
66 dealer.line2.1
67 dealer.line2.2
68 dealer.line2.3
69 inspect.flip.back
70 inspect.flip.front
@@ -65,3 +65,5 @@ dealer.line1.3,"路上收来的货,一手价。"
dealer.line2.1,"自己看,背面也翻过来看。"
dealer.line2.2,"不保修,看好了再拿。"
dealer.line2.3,"要的话价钱好商量。"
inspect.flip.back,"翻到背面"
inspect.flip.front,"翻到正面"
1 # Probably Decentralized 本地化表:简体中文(zh)
65 dealer.line2.1
66 dealer.line2.2
67 dealer.line2.3
68 inspect.flip.back
69 inspect.flip.front