Skip to content

Commit e1b5b08

Browse files
committed
feat: add coordinate screenshots and utility search
1 parent 406a8a9 commit e1b5b08

12 files changed

Lines changed: 1659 additions & 43 deletions

docs/api-developer-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ curl -X POST http://localhost:8787/mcp/ \
6666
"resources": { "subscribe": false, "listChanged": false },
6767
"tasks": { "list": {}, "cancel": {}, "requests": { "tools": { "call": {} } } }
6868
},
69-
"serverInfo": { "name": "OniMcp", "version": "0.1.6" }
69+
"serverInfo": { "name": "OniMcp", "version": "0.1.7" }
7070
}
7171
}
7272
```
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using TMPro;
5+
using UnityEngine;
6+
using UnityEngine.UI;
7+
8+
namespace OniMcp
9+
{
10+
internal sealed class CoordinateGridOverlay : MonoBehaviour
11+
{
12+
private const int SortingOrder = 31500;
13+
private const float LineWidth = 2f;
14+
private const int MaxLineObjects = 260;
15+
private const int MaxLabelObjects = 420;
16+
17+
private static readonly Color MajorLine = new Color(0.02f, 0.9f, 1f, 0.95f);
18+
private static readonly Color MinorLine = new Color(0.02f, 0.9f, 1f, 0.42f);
19+
private static readonly Color LabelFill = new Color(0.02f, 0.025f, 0.03f, 0.86f);
20+
private static readonly Color LabelText = new Color(0.93f, 1f, 1f, 1f);
21+
22+
private readonly List<Image> lines = new List<Image>();
23+
private readonly List<LabelVisual> labels = new List<LabelVisual>();
24+
private Canvas canvas;
25+
private RectTransform root;
26+
private OverlayRequest request;
27+
private Coroutine hideCoroutine;
28+
29+
public static CoordinateGridOverlay Instance { get; private set; }
30+
31+
public static void EnsureInstance()
32+
{
33+
if (Instance != null)
34+
return;
35+
36+
var obj = new GameObject("OniMcp_CoordinateGridOverlay");
37+
DontDestroyOnLoad(obj);
38+
Instance = obj.AddComponent<CoordinateGridOverlay>();
39+
}
40+
41+
public static void Show(OverlayRequest request)
42+
{
43+
EnsureInstance();
44+
Instance.request = request;
45+
Instance.EnsureCanvas();
46+
Instance.canvas.enabled = true;
47+
Instance.Refresh();
48+
if (Instance.hideCoroutine != null)
49+
Instance.StopCoroutine(Instance.hideCoroutine);
50+
Instance.hideCoroutine = Instance.StartCoroutine(Instance.HideAfter(request.VisibleSeconds));
51+
}
52+
53+
public static void Hide()
54+
{
55+
if (Instance == null)
56+
return;
57+
Instance.SetVisibleCount(0, 0);
58+
if (Instance.canvas != null)
59+
Instance.canvas.enabled = false;
60+
Instance.request = null;
61+
}
62+
63+
private void Awake()
64+
{
65+
Instance = this;
66+
}
67+
68+
private void LateUpdate()
69+
{
70+
if (request != null && canvas != null && canvas.enabled)
71+
Refresh();
72+
}
73+
74+
private IEnumerator HideAfter(float seconds)
75+
{
76+
int frames = Math.Max(8, Mathf.CeilToInt(Mathf.Max(0.25f, seconds) * 60f));
77+
for (int i = 0; i < frames; i++)
78+
yield return null;
79+
Hide();
80+
}
81+
82+
private void EnsureCanvas()
83+
{
84+
if (canvas != null)
85+
return;
86+
87+
var canvasObj = new GameObject("OniMcp_CoordinateGridCanvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler));
88+
canvasObj.transform.SetParent(transform, false);
89+
root = canvasObj.GetComponent<RectTransform>();
90+
root.anchorMin = Vector2.zero;
91+
root.anchorMax = Vector2.one;
92+
root.pivot = new Vector2(0f, 1f);
93+
root.offsetMin = Vector2.zero;
94+
root.offsetMax = Vector2.zero;
95+
96+
canvas = canvasObj.GetComponent<Canvas>();
97+
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
98+
canvas.sortingOrder = SortingOrder;
99+
canvas.enabled = false;
100+
101+
var scaler = canvasObj.GetComponent<CanvasScaler>();
102+
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
103+
}
104+
105+
private void Refresh()
106+
{
107+
if (request == null || Camera.main == null)
108+
return;
109+
110+
int lineCount = 0;
111+
int labelCount = 0;
112+
int step = Math.Max(1, request.Step);
113+
int majorEvery = Math.Max(step, request.MajorEvery);
114+
115+
if (request.ShowGrid)
116+
{
117+
for (int x = request.X1; x <= request.X2 + 1 && lineCount < MaxLineObjects; x++)
118+
{
119+
float worldX = x - 0.5f;
120+
if (TryWorldLineToScreen(worldX, request.Y1 - 0.5f, worldX, request.Y2 + 0.5f, out Rect rect))
121+
UpdateLine(lineCount++, rect, ((x - request.X1) % majorEvery == 0) ? MajorLine : MinorLine);
122+
}
123+
124+
for (int y = request.Y1; y <= request.Y2 + 1 && lineCount < MaxLineObjects; y++)
125+
{
126+
float worldY = y - 0.5f;
127+
if (TryWorldLineToScreen(request.X1 - 0.5f, worldY, request.X2 + 0.5f, worldY, out Rect rect))
128+
UpdateLine(lineCount++, rect, ((y - request.Y1) % majorEvery == 0) ? MajorLine : MinorLine);
129+
}
130+
}
131+
132+
if (request.ShowCoordinates)
133+
{
134+
for (int x = request.X1; x <= request.X2 && labelCount < MaxLabelObjects; x += step)
135+
{
136+
if (TryWorldToTopLeft(x, request.Y2 + 0.65f, out Vector2 top))
137+
UpdateLabel(labelCount++, x.ToString(), top.x - 18f, top.y - 18f, 42f, 20f);
138+
if (TryWorldToTopLeft(x, request.Y1 - 0.65f, out Vector2 bottom))
139+
UpdateLabel(labelCount++, x.ToString(), bottom.x - 18f, bottom.y - 2f, 42f, 20f);
140+
}
141+
142+
for (int y = request.Y1; y <= request.Y2 && labelCount < MaxLabelObjects; y += step)
143+
{
144+
if (TryWorldToTopLeft(request.X1 - 0.85f, y, out Vector2 left))
145+
UpdateLabel(labelCount++, y.ToString(), left.x - 42f, left.y - 10f, 40f, 20f);
146+
if (TryWorldToTopLeft(request.X2 + 0.85f, y, out Vector2 right))
147+
UpdateLabel(labelCount++, y.ToString(), right.x + 2f, right.y - 10f, 40f, 20f);
148+
}
149+
}
150+
151+
if (request.IncludeCellLabels)
152+
{
153+
int cells = Math.Max(1, (request.X2 - request.X1 + 1) * (request.Y2 - request.Y1 + 1));
154+
int labelStep = Math.Max(step, Mathf.CeilToInt(Mathf.Sqrt(cells / 140f)));
155+
for (int y = request.Y1; y <= request.Y2 && labelCount < MaxLabelObjects; y += labelStep)
156+
{
157+
for (int x = request.X1; x <= request.X2 && labelCount < MaxLabelObjects; x += labelStep)
158+
{
159+
if (TryWorldToTopLeft(x, y, out Vector2 center))
160+
UpdateLabel(labelCount++, x + "," + y, center.x - 28f, center.y - 9f, 56f, 18f);
161+
}
162+
}
163+
}
164+
165+
SetVisibleCount(lineCount, labelCount);
166+
}
167+
168+
private bool TryWorldLineToScreen(float x1, float y1, float x2, float y2, out Rect rect)
169+
{
170+
rect = default(Rect);
171+
if (!TryWorldToTopLeft(x1, y1, out Vector2 a) || !TryWorldToTopLeft(x2, y2, out Vector2 b))
172+
return false;
173+
174+
float xMin = Mathf.Min(a.x, b.x);
175+
float yMin = Mathf.Min(a.y, b.y);
176+
float width = Mathf.Max(LineWidth, Mathf.Abs(a.x - b.x));
177+
float height = Mathf.Max(LineWidth, Mathf.Abs(a.y - b.y));
178+
rect = new Rect(xMin, yMin, width, height);
179+
return rect.xMax >= -20f && rect.yMax >= -20f && rect.xMin <= Screen.width + 20f && rect.yMin <= Screen.height + 20f;
180+
}
181+
182+
private bool TryWorldToTopLeft(float x, float y, out Vector2 screen)
183+
{
184+
screen = Vector2.zero;
185+
var point = Camera.main.WorldToScreenPoint(new Vector3(x, y, 0f));
186+
if (point.z <= 0f)
187+
return false;
188+
screen = new Vector2(point.x, Screen.height - point.y);
189+
return true;
190+
}
191+
192+
private void UpdateLine(int index, Rect rect, Color color)
193+
{
194+
while (lines.Count <= index)
195+
lines.Add(CreateLine());
196+
var line = lines[index];
197+
line.gameObject.SetActive(true);
198+
line.color = color;
199+
SetTopLeftRect(line.rectTransform, rect.xMin, rect.yMin, rect.width, rect.height);
200+
}
201+
202+
private Image CreateLine()
203+
{
204+
var obj = new GameObject("CoordGridLine", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
205+
obj.transform.SetParent(canvas.transform, false);
206+
var image = obj.GetComponent<Image>();
207+
image.raycastTarget = false;
208+
ConfigureTopLeft(image.rectTransform);
209+
return image;
210+
}
211+
212+
private void UpdateLabel(int index, string text, float x, float y, float width, float height)
213+
{
214+
while (labels.Count <= index)
215+
labels.Add(CreateLabel());
216+
var label = labels[index];
217+
label.Root.SetActive(true);
218+
label.Background.color = LabelFill;
219+
label.Text.color = LabelText;
220+
label.Text.SetText(text);
221+
SetTopLeftRect(label.Rect, x, y, width, height);
222+
}
223+
224+
private LabelVisual CreateLabel()
225+
{
226+
var rootObj = new GameObject("CoordGridLabel", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
227+
rootObj.transform.SetParent(canvas.transform, false);
228+
var rect = rootObj.GetComponent<RectTransform>();
229+
ConfigureTopLeft(rect);
230+
231+
var background = rootObj.GetComponent<Image>();
232+
background.raycastTarget = false;
233+
234+
var textObj = new GameObject("Text", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
235+
textObj.transform.SetParent(rootObj.transform, false);
236+
var textRect = textObj.GetComponent<RectTransform>();
237+
textRect.anchorMin = Vector2.zero;
238+
textRect.anchorMax = Vector2.one;
239+
textRect.offsetMin = new Vector2(2f, 1f);
240+
textRect.offsetMax = new Vector2(-2f, -1f);
241+
242+
var text = textObj.GetComponent<TextMeshProUGUI>();
243+
text.raycastTarget = false;
244+
text.richText = false;
245+
text.fontSize = 12f;
246+
text.alignment = TextAlignmentOptions.Center;
247+
text.overflowMode = TextOverflowModes.Ellipsis;
248+
text.textWrappingMode = TextWrappingModes.NoWrap;
249+
if (Localization.FontAsset != null)
250+
text.font = Localization.FontAsset;
251+
252+
return new LabelVisual { Root = rootObj, Rect = rect, Background = background, Text = text };
253+
}
254+
255+
private void SetVisibleCount(int lineCount, int labelCount)
256+
{
257+
for (int i = lineCount; i < lines.Count; i++)
258+
lines[i].gameObject.SetActive(false);
259+
for (int i = labelCount; i < labels.Count; i++)
260+
labels[i].Root.SetActive(false);
261+
}
262+
263+
private static void ConfigureTopLeft(RectTransform rect)
264+
{
265+
rect.anchorMin = new Vector2(0f, 1f);
266+
rect.anchorMax = new Vector2(0f, 1f);
267+
rect.pivot = new Vector2(0f, 1f);
268+
}
269+
270+
private static void SetTopLeftRect(RectTransform rect, float x, float y, float width, float height)
271+
{
272+
rect.anchoredPosition = new Vector2(x, -y);
273+
rect.sizeDelta = new Vector2(Mathf.Max(0f, width), Mathf.Max(0f, height));
274+
}
275+
276+
public sealed class OverlayRequest
277+
{
278+
public int X1;
279+
public int Y1;
280+
public int X2;
281+
public int Y2;
282+
public int Step = 5;
283+
public int MajorEvery = 10;
284+
public bool ShowGrid = true;
285+
public bool ShowCoordinates = true;
286+
public bool IncludeCellLabels;
287+
public float VisibleSeconds = 2f;
288+
}
289+
290+
private sealed class LabelVisual
291+
{
292+
public GameObject Root;
293+
public RectTransform Rect;
294+
public Image Background;
295+
public TextMeshProUGUI Text;
296+
}
297+
}
298+
}

mods/oni_mcp/ModInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public static void Postfix()
6565
EditMarkerTool.EnsureInstance();
6666
PlanningViewOverlay.EnsureInstance();
6767
AgentPointerOverlay.EnsureInstance();
68+
CoordinateGridOverlay.EnsureInstance();
6869

6970
OniMcpLog.Debug("[OniMcp] Game-specific components initialized.");
7071
}

mods/oni_mcp/OniMcp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<ModTitle>ONI MCP Server</ModTitle>
2222
<ModDescription>Oxygen Not Included MCP Server - 将游戏操作封装为 Model Context Protocol 服务,使用 Streamable HTTP 传输层</ModDescription>
2323
<ModStaticID>LIghtJUNction.OniMcp</ModStaticID>
24-
<ModVersion>0.1.6</ModVersion>
24+
<ModVersion>0.1.7</ModVersion>
2525

2626
<!-- mod_info.yaml 元数据 -->
2727
<ModApiVersion>2</ModApiVersion>

mods/oni_mcp/Server/McpHttpServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ private object HandleInitialize(object id, InitializeParams @params, string sess
737737
ServerInfo = new Implementation
738738
{
739739
Name = "OniMcp",
740-
Version = "0.1.6"
740+
Version = "0.1.7"
741741
}
742742
};
743743
}

0 commit comments

Comments
 (0)