-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRPGDevTray.cs
More file actions
255 lines (236 loc) · 11.5 KB
/
Copy pathRPGDevTray.cs
File metadata and controls
255 lines (236 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
// RPGDev タスクトレイ常駐(Windows)。
// 目的:ハブ(rpgdev サーバ)が起動しているかを一目で分かるようにする。常駐していれば稼働中、消えていれば停止。
// アイコンは水の精霊 Aqua の「顔」をスプライト PNG から実行時に切り出して使う(外部の画像ツール不要=System.Drawing)。
// /health を定期監視し、ハブが落ちたら自分も退場する(=トレイの有無 = ハブの稼働)。右クリックで窓を開く/街に戻る/終了。
// ビルド・起動は scripts/desktop.mjs が csc で行う(RPGDevWindow.cs と同型)。窓 exe(RPGDev.exe) とは別プロセス(RPGDevTray.exe)。
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace RPGDevTray
{
internal static class Program
{
private static string _hubUrl;
private static string _instanceKey;
private static string _windowExe;
private static string _windowUrl;
private static string _windowData;
private static string _windowState;
private static NotifyIcon _icon;
private static int _healthFails;
private static FileStream _lock; // 単一インスタンスロック(プロセス生存中保持)
[DllImport("user32.dll")]
private static extern bool DestroyIcon(IntPtr handle);
[STAThread]
private static void Main(string[] args)
{
// アイコン生成モード:スタートメニューのショートカット用に Aqua の顔から .ico を書き出して終了。
// RPGDevTray.exe --make-ico <spritePng> <outIco>
if (args.Length >= 3 && args[0] == "--make-ico")
{
MakeIco(args[1], args[2]);
return;
}
_hubUrl = args.Length > 0 && args[0].Length > 0 ? args[0].TrimEnd('/') : "http://127.0.0.1:37373";
string spritePath = args.Length > 1 ? args[1] : "";
_instanceKey = args.Length > 2 && args[2].Length > 0 ? args[2] : "rpgdev-hub";
_windowExe = args.Length > 3 ? args[3] : "";
_windowUrl = args.Length > 4 ? args[4] : _hubUrl + "/overlay.html";
_windowData = args.Length > 5 ? args[5] : "";
_windowState = args.Length > 6 ? args[6] : "";
// 単一インスタンス:ハブ dir(窓 state と同じ場所)の lock ファイルを排他で握る。窓のファイルロックと同型。
string hubDir = _windowState.Length > 0 ? Path.GetDirectoryName(_windowState) : Path.GetTempPath();
if (string.IsNullOrEmpty(hubDir)) hubDir = Path.GetTempPath();
try { Directory.CreateDirectory(hubDir); }
catch { }
try
{
_lock = new FileStream(Path.Combine(hubDir, _instanceKey + ".tray.lock"),
FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 8, FileOptions.DeleteOnClose);
}
catch (IOException)
{
return; // 既にトレイが常駐している=二重に出さない
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
_icon = new NotifyIcon();
_icon.Icon = BuildAquaFaceIcon(spritePath);
_icon.Text = "RPGDev — ハブ稼働中"; // tooltip(最大63文字)
_icon.Visible = true;
_icon.DoubleClick += delegate { OpenWindow(); };
ContextMenuStrip menu = new ContextMenuStrip();
menu.Items.Add("ウィンドウを開く", null, delegate { OpenWindow(); });
menu.Items.Add("街に戻る", null, delegate { Post("/control/return-town"); });
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("終了(ハブを停止)", null, delegate { QuitHub(); });
_icon.ContextMenuStrip = menu;
// /health 監視:3秒ごと。連続で落ちたらハブ停止と見なしてトレイも退場する。
// System.Windows.Forms.Timer を明示(System.Threading.Timer と曖昧にしない)。
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
timer.Interval = 3000;
timer.Tick += delegate { CheckHealth(); };
timer.Start();
Application.ApplicationExit += delegate
{
try { _icon.Visible = false; _icon.Dispose(); }
catch { }
};
Application.Run();
GC.KeepAlive(_lock);
}
// Aqua スプライト PNG の「顔」(上部中央)を機械的に切り出して 32x32 アイコンにする。
// 画像内容は創作・改変しない(切り出し・縮小のみ=許可された機械処理)。読めない時は黙ってそれっぽい別物を作らず、
// システムアプリアイコンで代替(常駐=稼働の表示は維持)。
private static Icon BuildAquaFaceIcon(string spritePath)
{
try
{
using (Bitmap src = new Bitmap(spritePath))
{
int w = src.Width, h = src.Height;
int side = (int)(Math.Min(w, h) * 0.24); // 顔まわりの正方形
int cx = (int)(w * 0.52); // 顔の中心(3/4 後ろ向きで上部中央やや右)
int cy = (int)(h * 0.15);
int x = Math.Max(0, Math.Min(w - side, cx - side / 2));
int y = Math.Max(0, Math.Min(h - side, cy - side / 2));
using (Bitmap face = new Bitmap(32, 32, PixelFormat.Format32bppArgb))
{
using (Graphics g = Graphics.FromImage(face))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.DrawImage(src, new Rectangle(0, 0, 32, 32), new Rectangle(x, y, side, side), GraphicsUnit.Pixel);
}
IntPtr hicon = face.GetHicon();
try { return (Icon)Icon.FromHandle(hicon).Clone(); }
finally { DestroyIcon(hicon); }
}
}
}
catch
{
return SystemIcons.Application; // スプライトが読めない環境でも常駐表示は出す(顔は出ないが稼働は分かる)
}
}
// 顔を 256x256 に切り出し、PNG-in-ICO(Vista+ が読む形式)として .ico を書き出す。
// スタートメニューのショートカット用。画像内容は改変せず切り出し・縮小のみ。
private static void MakeIco(string spritePath, string outPath)
{
using (Bitmap src = new Bitmap(spritePath))
{
int w = src.Width, h = src.Height;
int side = (int)(Math.Min(w, h) * 0.24);
int cx = (int)(w * 0.52);
int cy = (int)(h * 0.15);
int x = Math.Max(0, Math.Min(w - side, cx - side / 2));
int y = Math.Max(0, Math.Min(h - side, cy - side / 2));
using (Bitmap face = new Bitmap(256, 256, PixelFormat.Format32bppArgb))
{
using (Graphics g = Graphics.FromImage(face))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.DrawImage(src, new Rectangle(0, 0, 256, 256), new Rectangle(x, y, side, side), GraphicsUnit.Pixel);
}
byte[] png;
using (MemoryStream ms = new MemoryStream())
{
face.Save(ms, ImageFormat.Png);
png = ms.ToArray();
}
using (FileStream fs = new FileStream(outPath, FileMode.Create, FileAccess.Write))
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write((short)0); // reserved
bw.Write((short)1); // type = icon
bw.Write((short)1); // image count
bw.Write((byte)0); // width 0 = 256
bw.Write((byte)0); // height 0 = 256
bw.Write((byte)0); // palette
bw.Write((byte)0); // reserved
bw.Write((short)1); // color planes
bw.Write((short)32); // bits per pixel
bw.Write(png.Length); // size of image data
bw.Write(22); // offset (6 + 16)
bw.Write(png);
}
}
}
}
private static void OpenWindow()
{
if (_windowExe.Length == 0 || !File.Exists(_windowExe)) return;
try
{
Process.Start(new ProcessStartInfo(_windowExe,
Quote(_windowUrl) + " " + Quote(_windowData) + " " + Quote(_windowState) + " " + Quote(_instanceKey))
{ UseShellExecute = false });
}
catch { }
}
private static void QuitHub()
{
Post("/control/shutdown"); // ハブをきれいに停止(サーバ側 process.exit)
try
{
foreach (Process p in Process.GetProcessesByName("RPGDev")) // 窓も閉じる(トレイ RPGDevTray は対象外)
{
try { p.Kill(); } catch { }
}
}
catch { }
Application.Exit();
}
private static void CheckHealth()
{
if (HttpOk(_hubUrl + "/health"))
{
_healthFails = 0;
_icon.Text = "RPGDev — ハブ稼働中";
}
else
{
_healthFails += 1;
_icon.Text = "RPGDev — 応答なし…";
if (_healthFails >= 3) Application.Exit(); // ハブ停止=トレイも消える
}
}
private static bool HttpOk(string url)
{
try
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.Method = "GET";
req.Timeout = 1500;
using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
{
return (int)res.StatusCode >= 200 && (int)res.StatusCode < 300;
}
}
catch { return false; }
}
private static void Post(string path)
{
try
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(_hubUrl + path);
req.Method = "POST";
req.Timeout = 2000;
req.ContentLength = 0;
using (req.GetResponse()) { }
}
catch { }
}
private static string Quote(string value)
{
return "\"" + (value ?? "").Replace("\"", "\\\"") + "\"";
}
}
}