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.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEngine;
using Decentralized.Domain; using Decentralized.Domain;
namespace Decentralized namespace Decentralized
@@ -23,6 +24,20 @@ namespace Decentralized
Items.Give(Items.NewCard("RZ6800", 30, "RZ580", rng)); Items.Give(Items.NewCard("RZ6800", 30, "RZ580", rng));
Items.Give(Items.NewCard("RZ7900", 90, "RZ470", 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)", () => ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
{ {
EmporiumEntry.Instance.ClearFrontInv(); 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)); var item = ItemDirectory.CreateEmptyItem(GpuId(spec.Model));
item.SetGameItemType("MATERIAL"); item.SetGameItemType("MATERIAL");
InspectableHelper.InitInspectableItem(item);
item.onLoaded = Refresh; item.onLoaded = Refresh;
// 右键"切换"翻面;外观差异用叠加图画在同一张底图上 // 右键"切换"翻面;外观差异用叠加图画在同一张底图上
item.EnableTag(ToggleAppearanceHelper.TOGGLE_APPEARANCE_TAG); item.EnableTag(ToggleAppearanceHelper.TOGGLE_APPEARANCE_TAG);
@@ -77,8 +78,7 @@ namespace Decentralized
item.canToggleSlotItemFunc = (_, _, _) => true; item.canToggleSlotItemFunc = (_, _, _) => true;
item.onToggleSlotItemFunc = (toggled, _, _) => Log.Guard("显卡翻面", () => item.onToggleSlotItemFunc = (toggled, _, _) => Log.Guard("显卡翻面", () =>
{ {
if (toggled.IsTag(BackTag)) toggled.ModifyTag(BackTag, t => t.Disable()); else toggled.EnableTag(BackTag); Flip(toggled);
Refresh(toggled);
}); });
item.getItemSpriteModifierForItemFunc = Overlays; item.getItemSpriteModifierForItemFunc = Overlays;
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name); ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
@@ -152,33 +152,51 @@ namespace Decentralized
SyncValue(item); 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)); if (IsBack(item)) item.ModifyTag(BackTag, t => t.Disable()); else item.EnableTag(BackTag);
return mod != null && mod.Sprites.TryGetValue(key, out var sprite) ? sprite : null; Refresh(item);
} }
/// <summary>外观叠加图:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary> /// <summary>当前面的底图名与叠加图名:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
private static List<ItemSpriteModifier> Overlays(GameItem item) 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 u = ToUnit(item);
var keys = new List<string>(); bool back = IsBack(item);
if (!item.IsTag(BackTag)) 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.Fan > 0) overlays.Add("gpu_ov_fan" + u.Look.Fan);
if (u.Look.Fingers > 0) keys.Add("gpu_ov_fin" + u.Look.Fingers); if (u.Look.Fingers > 0) overlays.Add("gpu_ov_fin" + u.Look.Fingers);
if (u.Look.Bracket > 0) keys.Add("gpu_ov_brk" + u.Look.Bracket); if (u.Look.Bracket > 0) overlays.Add("gpu_ov_brk" + u.Look.Bracket);
} }
else else
{ {
// 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过) // 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过)
string model = u.Look.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model; 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()); overlays.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); 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; int priority = 0;
foreach (var k in keys) foreach (var k in keys)
{ {
+2
View File
@@ -16,6 +16,7 @@ namespace Decentralized
Night.Subscribe(); Night.Subscribe();
DealerStock.Subscribe(); DealerStock.Subscribe();
UsedDealerClient.Subscribe(); UsedDealerClient.Subscribe();
GpuInspectView.Ensure();
ModHook.OnGameLoadedLate += Session.Load; ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame; ModHook.OnNewGameStarted += OnNewGame;
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用"); Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
@@ -27,6 +28,7 @@ namespace Decentralized
Night.Unsubscribe(); Night.Unsubscribe();
DealerStock.Unsubscribe(); DealerStock.Unsubscribe();
UsedDealerClient.Unsubscribe(); UsedDealerClient.Unsubscribe();
GpuInspectView.Remove();
ModHook.OnGameLoadedLate -= Session.Load; ModHook.OnGameLoadedLate -= Session.Load;
ModHook.OnNewGameStarted -= OnNewGame; 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.1,"See for yourself, turn them over and check the back."
dealer.line2.2,"No warranty. Look before you take." dealer.line2.2,"No warranty. Look before you take."
dealer.line2.3,"Price is negotiable if you want them." 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.1,"自己看,背面也翻过来看。"
dealer.line2.2,"不保修,看好了再拿。" dealer.line2.2,"不保修,看好了再拿。"
dealer.line2.3,"要的话价钱好商量。" 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