@ganziliang/desktop-pet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/assets/awaken/README.md +82 -0
- package/assets/awaken/astral-burst.png +0 -0
- package/assets/awaken/astral-charge.png +0 -0
- package/assets/awaken/nocturne-burst.png +0 -0
- package/assets/awaken/nocturne-charge.png +0 -0
- package/assets/awaken/seraph-burst.png +0 -0
- package/assets/awaken/seraph-charge.png +0 -0
- package/assets/awaken/thunder-burst.png +0 -0
- package/assets/awaken/thunder-charge.png +0 -0
- package/assets/outfits.json +18 -0
- package/assets/pet-base.png +0 -0
- package/assets/pet-blink.png +0 -0
- package/assets/pet-idle-blink.png +0 -0
- package/assets/pet-idle.png +0 -0
- package/assets/pet-run.png +0 -0
- package/assets/pet-swim.png +0 -0
- package/assets/pet-walk.png +0 -0
- package/assets/sprites.json +132 -0
- package/bin/desktop-pet.js +43 -0
- package/examples/pi-pet-bridge.ts +101 -0
- package/main.js +1143 -0
- package/package.json +58 -0
- package/preload.js +29 -0
- package/renderer/app.js +1219 -0
- package/renderer/awaken.js +775 -0
- package/renderer/fx.js +1057 -0
- package/renderer/index.html +43 -0
- package/renderer/style.css +599 -0
- package/scripts/debug-run.ps1 +6 -0
- package/scripts/drop-probe.ps1 +120 -0
- package/scripts/hold-probe.ps1 +86 -0
- package/scripts/knockout.py +98 -0
- package/scripts/roam-probe.ps1 +95 -0
- package/scripts/sim-drag.ps1 +107 -0
- package/scripts/spritesheet.py +425 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# drop-probe.ps1 -- does the pet stay where you drop it, or fall to the bottom?
|
|
2
|
+
#
|
|
3
|
+
# Presses on the character, drags it UP, releases, then waits and re-reads the
|
|
4
|
+
# window rect. With gravity off it must stay where it was released; with gravity
|
|
5
|
+
# on it must end up on the bottom edge of the work area.
|
|
6
|
+
#
|
|
7
|
+
# NOTE: keep this file pure ASCII (PowerShell 5.1 reads .ps1 as ANSI).
|
|
8
|
+
|
|
9
|
+
param(
|
|
10
|
+
[string]$Hwnd,
|
|
11
|
+
[int]$RisePx = 260,
|
|
12
|
+
[int]$SettleMs = 1500,
|
|
13
|
+
[ValidateSet('stay','fall')][string]$Expect = 'stay'
|
|
14
|
+
)
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
$root = Split-Path -Parent $PSScriptRoot
|
|
17
|
+
$report = Join-Path $root 'drop-probe-report.txt'
|
|
18
|
+
$out = New-Object System.Collections.Generic.List[string]
|
|
19
|
+
function Say($t) { $out.Add($t) | Out-Null; Write-Host $t }
|
|
20
|
+
|
|
21
|
+
Add-Type @"
|
|
22
|
+
using System;
|
|
23
|
+
using System.Runtime.InteropServices;
|
|
24
|
+
public class PetDrop {
|
|
25
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
26
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
|
|
27
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
28
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
|
|
29
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
|
30
|
+
}
|
|
31
|
+
"@
|
|
32
|
+
[void][PetDrop]::SetProcessDPIAware()
|
|
33
|
+
|
|
34
|
+
$MOVE = 0x0001; $LDOWN = 0x0002; $LUP = 0x0004
|
|
35
|
+
|
|
36
|
+
$health = Invoke-RestMethod -Uri 'http://127.0.0.1:8520/health' -TimeoutSec 3
|
|
37
|
+
if (-not $Hwnd -and $health.hwnd) { $Hwnd = [string]$health.hwnd }
|
|
38
|
+
if (-not $Hwnd) { Say 'FAIL: no hwnd'; $out | Set-Content -Encoding ASCII $report; exit 1 }
|
|
39
|
+
|
|
40
|
+
# take manual control so autonomous roaming does not fight the measurement, but remember
|
|
41
|
+
# the previous value: the setting is persisted, so leaving it off would silently turn the
|
|
42
|
+
# pet's autonomous behaviour off for good.
|
|
43
|
+
$autoWas = $health.autoRoam
|
|
44
|
+
if ($null -eq $autoWas) { $autoWas = $true }
|
|
45
|
+
Invoke-RestMethod -Uri 'http://127.0.0.1:8520/auto' -Method Post -ContentType 'application/json' -Body '{"enabled":false}' | Out-Null
|
|
46
|
+
Start-Sleep -Milliseconds 400
|
|
47
|
+
|
|
48
|
+
$h = [IntPtr][long]$Hwnd
|
|
49
|
+
$r1 = New-Object PetDrop+RECT
|
|
50
|
+
[void][PetDrop]::GetWindowRect($h, [ref]$r1)
|
|
51
|
+
$w = $r1.Right - $r1.Left
|
|
52
|
+
$hgt = $r1.Bottom - $r1.Top
|
|
53
|
+
Say ("before : ({0},{1}) size {2}x{3}" -f $r1.Left, $r1.Top, $w, $hgt)
|
|
54
|
+
|
|
55
|
+
$px = [int]($r1.Left + $w * 0.5)
|
|
56
|
+
$py = [int]($r1.Top + $hgt * 0.62)
|
|
57
|
+
[void][PetDrop]::SetCursorPos($px, $py)
|
|
58
|
+
Start-Sleep -Milliseconds 250
|
|
59
|
+
[void][PetDrop]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero)
|
|
60
|
+
Start-Sleep -Milliseconds 180
|
|
61
|
+
|
|
62
|
+
$steps = 10
|
|
63
|
+
for ($i = 1; $i -le $steps; $i++) {
|
|
64
|
+
[void][PetDrop]::SetCursorPos($px, $py - [int]($RisePx * $i / $steps))
|
|
65
|
+
Start-Sleep -Milliseconds 35
|
|
66
|
+
}
|
|
67
|
+
[void][PetDrop]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
|
|
68
|
+
Say ("dropped {0}px above the start position" -f $RisePx)
|
|
69
|
+
|
|
70
|
+
Start-Sleep -Milliseconds $SettleMs
|
|
71
|
+
$r2 = New-Object PetDrop+RECT
|
|
72
|
+
[void][PetDrop]::GetWindowRect($h, [ref]$r2)
|
|
73
|
+
$rise = $r1.Top - $r2.Top # positive = ended up above the start position
|
|
74
|
+
Say ("after : ({0},{1}) rise={2}px above start" -f $r2.Left, $r2.Top, $rise)
|
|
75
|
+
|
|
76
|
+
# The drag is a burst of synthetic mouse events and may drop a frame or two, so do not
|
|
77
|
+
# require an exact landing point. Two things are worth asserting:
|
|
78
|
+
# stay -> it ended up well above where it started (never got pulled down)
|
|
79
|
+
# fall -> its bottom edge is sitting on the bottom of the work area
|
|
80
|
+
# Work area comes from /health in DIP; the window rect is physical, hence scaleFactor.
|
|
81
|
+
$cx = ($r2.Left + $r2.Right) / 2.0
|
|
82
|
+
$cy = ($r2.Top + $r2.Bottom) / 2.0
|
|
83
|
+
$disp = $null
|
|
84
|
+
foreach ($d in $health.displays) {
|
|
85
|
+
$sf = $d.scaleFactor
|
|
86
|
+
$dx = $cx / $sf; $dy = $cy / $sf
|
|
87
|
+
if ($dx -ge $d.bounds.x -and $dx -le ($d.bounds.x + $d.bounds.width) -and
|
|
88
|
+
$dy -ge $d.bounds.y -and $dy -le ($d.bounds.y + $d.bounds.height)) { $disp = $d; break }
|
|
89
|
+
}
|
|
90
|
+
$floorBottom = $null
|
|
91
|
+
if ($disp) {
|
|
92
|
+
$floorBottom = ($disp.workArea.y + $disp.workArea.height) * $disp.scaleFactor
|
|
93
|
+
Say ("work area bottom (physical) = {0}, window bottom = {1}" -f $floorBottom, $r2.Bottom)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$ok = $true
|
|
97
|
+
if ($Expect -eq 'stay') {
|
|
98
|
+
if ($rise -gt [int]($RisePx * 0.6)) {
|
|
99
|
+
Say 'RESULT: PASS - pet stayed where it was dropped'
|
|
100
|
+
} else {
|
|
101
|
+
Say ("RESULT: FAIL - pet did not stay up (rise={0}, expected about {1})" -f $rise, $RisePx)
|
|
102
|
+
$ok = $false
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
if ($null -ne $floorBottom -and [Math]::Abs($r2.Bottom - $floorBottom) -le 20) {
|
|
106
|
+
Say 'RESULT: PASS - pet fell back to the ground line'
|
|
107
|
+
} else {
|
|
108
|
+
Say 'RESULT: FAIL - pet is not sitting on the bottom of the work area'
|
|
109
|
+
$ok = $false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# always put the autonomous setting back, even when the check failed
|
|
114
|
+
if ($autoWas) {
|
|
115
|
+
Invoke-RestMethod -Uri 'http://127.0.0.1:8520/auto' -Method Post -ContentType 'application/json' -Body '{"enabled":true}' | Out-Null
|
|
116
|
+
Say 'restored autoRoam=true'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$out | Set-Content -Encoding ASCII $report
|
|
120
|
+
if (-not $ok) { exit 1 }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Hold-probe: press and HOLD the left button on the pet without moving the cursor,
|
|
2
|
+
# sampling the window rect every 200ms. Tells us whether the WINDOW grows or the
|
|
3
|
+
# rendered content is being zoomed. ASCII only (PS 5.1 reads .ps1 as ANSI).
|
|
4
|
+
param(
|
|
5
|
+
[string]$Hwnd,
|
|
6
|
+
[int]$HoldMs = 4000,
|
|
7
|
+
[int]$StepMs = 200,
|
|
8
|
+
[int]$JitterPx = 0,
|
|
9
|
+
[switch]$NoPress
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
$ErrorActionPreference = 'Stop'
|
|
13
|
+
$root = Split-Path -Parent $PSScriptRoot
|
|
14
|
+
$report = Join-Path $root 'hold-probe-report.txt'
|
|
15
|
+
$out = New-Object System.Collections.Generic.List[string]
|
|
16
|
+
function Say($t) { $out.Add($t) | Out-Null; Write-Host $t }
|
|
17
|
+
|
|
18
|
+
Add-Type @"
|
|
19
|
+
using System;
|
|
20
|
+
using System.Runtime.InteropServices;
|
|
21
|
+
public class HoldWin {
|
|
22
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
23
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
|
|
24
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
25
|
+
[DllImport("user32.dll")] public static extern bool GetCursorPos(out RECTpt p);
|
|
26
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
|
|
27
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
|
28
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECTpt { public int X; public int Y; }
|
|
29
|
+
}
|
|
30
|
+
"@
|
|
31
|
+
|
|
32
|
+
[void][HoldWin]::SetProcessDPIAware()
|
|
33
|
+
|
|
34
|
+
$LDOWN = 0x0002
|
|
35
|
+
$LUP = 0x0004
|
|
36
|
+
|
|
37
|
+
if (-not $Hwnd) {
|
|
38
|
+
try {
|
|
39
|
+
$health = Invoke-RestMethod -Uri 'http://127.0.0.1:8520/health' -TimeoutSec 3
|
|
40
|
+
if ($health.hwnd) { $Hwnd = [string]$health.hwnd }
|
|
41
|
+
} catch { }
|
|
42
|
+
}
|
|
43
|
+
if (-not $Hwnd) { Say 'FAIL: no hwnd (is the app running?)'; $out | Set-Content -Encoding ASCII $report; exit 1 }
|
|
44
|
+
|
|
45
|
+
$h = [IntPtr][long]$Hwnd
|
|
46
|
+
$r = New-Object HoldWin+RECT
|
|
47
|
+
[void][HoldWin]::GetWindowRect($h, [ref]$r)
|
|
48
|
+
Say ("HWND={0}" -f $Hwnd)
|
|
49
|
+
Say ("initial: ({0},{1})-({2},{3}) size {4}x{5}" -f $r.Left, $r.Top, $r.Right, $r.Bottom, ($r.Right - $r.Left), ($r.Bottom - $r.Top))
|
|
50
|
+
|
|
51
|
+
$px = [int]($r.Left + ($r.Right - $r.Left) * 0.50)
|
|
52
|
+
$py = [int]($r.Top + ($r.Bottom - $r.Top) * 0.58)
|
|
53
|
+
Say ("press point: ({0},{1})" -f $px, $py)
|
|
54
|
+
|
|
55
|
+
[void][HoldWin]::SetCursorPos($px, $py)
|
|
56
|
+
Start-Sleep -Milliseconds 400
|
|
57
|
+
$c = New-Object HoldWin+RECTpt
|
|
58
|
+
[void][HoldWin]::GetCursorPos([ref]$c)
|
|
59
|
+
Say ("cursor: {0},{1}" -f $c.X, $c.Y)
|
|
60
|
+
|
|
61
|
+
if (-not $NoPress) { [HoldWin]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero) }
|
|
62
|
+
|
|
63
|
+
$n = [int]($HoldMs / $StepMs)
|
|
64
|
+
for ($i = 1; $i -le $n; $i++) {
|
|
65
|
+
if ($JitterPx -gt 0) {
|
|
66
|
+
$d = if ($i % 2 -eq 0) { $JitterPx } else { -$JitterPx }
|
|
67
|
+
[HoldWin]::mouse_event(0x0001, $d, 0, 0, [IntPtr]::Zero)
|
|
68
|
+
}
|
|
69
|
+
Start-Sleep -Milliseconds $StepMs
|
|
70
|
+
$s = New-Object HoldWin+RECT
|
|
71
|
+
[void][HoldWin]::GetWindowRect($h, [ref]$s)
|
|
72
|
+
[void][HoldWin]::GetCursorPos([ref]$c)
|
|
73
|
+
Say ("t={0,5}ms rect=({1},{2})-({3},{4}) size {5}x{6} cursor=({7},{8})" -f `
|
|
74
|
+
($i * $StepMs), $s.Left, $s.Top, $s.Right, $s.Bottom, ($s.Right - $s.Left), ($s.Bottom - $s.Top), $c.X, $c.Y)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (-not $NoPress) {
|
|
78
|
+
[HoldWin]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
|
|
79
|
+
Start-Sleep -Milliseconds 600
|
|
80
|
+
$e = New-Object HoldWin+RECT
|
|
81
|
+
[void][HoldWin]::GetWindowRect($h, [ref]$e)
|
|
82
|
+
Say ("after release: size {0}x{1}" -f ($e.Right - $e.Left), ($e.Bottom - $e.Top))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$out | Set-Content -Encoding ASCII $report
|
|
86
|
+
Write-Host ("report: {0}" -f $report)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""绿幕抠图:把纯绿背景的动漫立绘变成带 alpha 的通明 PNG。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python scripts/knockout.py assets/character_green.png assets/character.png --height 640
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from PIL import Image
|
|
14
|
+
|
|
15
|
+
try: # numpy 有就快很多
|
|
16
|
+
import numpy as np
|
|
17
|
+
except ImportError: # pragma: no cover
|
|
18
|
+
np = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def key_out(img: Image.Image, lo: int, hi: int) -> Image.Image:
|
|
22
|
+
"""greenness = G - max(R, B),越大越绿。lo 以下保留,hi 以上全透明,中间做软过渡。"""
|
|
23
|
+
img = img.convert("RGBA")
|
|
24
|
+
|
|
25
|
+
if np is not None:
|
|
26
|
+
arr = np.asarray(img).astype(np.int16)
|
|
27
|
+
r, g, b, a = arr[..., 0], arr[..., 1], arr[..., 2], arr[..., 3]
|
|
28
|
+
greenness = g - np.maximum(r, b)
|
|
29
|
+
ramp = np.clip((hi - greenness) / float(hi - lo), 0.0, 1.0)
|
|
30
|
+
alpha = (a * ramp).astype(np.uint8)
|
|
31
|
+
# 边缘去绿溢色:半透明像素里把 G 压到不超过 max(R, B)
|
|
32
|
+
spill = (alpha < 250) & (g > np.maximum(r, b))
|
|
33
|
+
g = np.where(spill, np.maximum(r, b), g)
|
|
34
|
+
out = np.stack([r, g, b, alpha.astype(np.int16)], axis=-1).astype(np.uint8)
|
|
35
|
+
return Image.fromarray(out, "RGBA")
|
|
36
|
+
|
|
37
|
+
px = img.load()
|
|
38
|
+
out = Image.new("RGBA", img.size)
|
|
39
|
+
op = out.load()
|
|
40
|
+
for y in range(img.height):
|
|
41
|
+
for x in range(img.width):
|
|
42
|
+
r, g, b, a = px[x, y]
|
|
43
|
+
greenness = g - max(r, b)
|
|
44
|
+
if greenness <= lo:
|
|
45
|
+
alpha = a
|
|
46
|
+
elif greenness >= hi:
|
|
47
|
+
alpha = 0
|
|
48
|
+
else:
|
|
49
|
+
alpha = int(a * (hi - greenness) / float(hi - lo))
|
|
50
|
+
if 0 < alpha < 250 and g > max(r, b):
|
|
51
|
+
g = max(r, b)
|
|
52
|
+
op[x, y] = (r, g, b, alpha)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def trim(img: Image.Image, pad: int = 6) -> Image.Image:
|
|
57
|
+
bbox = img.getchannel("A").point(lambda v: 255 if v > 8 else 0).getbbox()
|
|
58
|
+
if not bbox:
|
|
59
|
+
return img
|
|
60
|
+
left, top, right, bottom = bbox
|
|
61
|
+
return img.crop(
|
|
62
|
+
(
|
|
63
|
+
max(0, left - pad),
|
|
64
|
+
max(0, top - pad),
|
|
65
|
+
min(img.width, right + pad),
|
|
66
|
+
min(img.height, bottom + pad),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main() -> None:
|
|
72
|
+
ap = argparse.ArgumentParser()
|
|
73
|
+
ap.add_argument("src")
|
|
74
|
+
ap.add_argument("dst")
|
|
75
|
+
ap.add_argument("--height", type=int, default=640, help="输出高度(等比缩放)")
|
|
76
|
+
ap.add_argument("--lo", type=int, default=30, help="绿度低于此值完全保留")
|
|
77
|
+
ap.add_argument("--hi", type=int, default=72, help="绿度高于此值完全透明")
|
|
78
|
+
args = ap.parse_args()
|
|
79
|
+
|
|
80
|
+
img = Image.open(args.src)
|
|
81
|
+
cut = key_out(img, args.lo, args.hi)
|
|
82
|
+
cut = trim(cut)
|
|
83
|
+
|
|
84
|
+
if args.height and cut.height > args.height:
|
|
85
|
+
w = round(cut.width * args.height / cut.height)
|
|
86
|
+
cut = cut.resize((w, args.height), Image.LANCZOS)
|
|
87
|
+
|
|
88
|
+
dst = Path(args.dst)
|
|
89
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
cut.save(dst, "PNG", optimize=True)
|
|
91
|
+
|
|
92
|
+
opaque = cut.getchannel("A").point(lambda v: 255 if v > 200 else 0)
|
|
93
|
+
coverage = sum(opaque.histogram()[255:]) / float(cut.width * cut.height)
|
|
94
|
+
print(f"[knockout] {dst} {cut.width}x{cut.height} 角色像素占比 {coverage:.1%}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# roam-probe.ps1 -- end-to-end check for the autonomous locomotion feature.
|
|
2
|
+
#
|
|
3
|
+
# It triggers a roam over the local HTTP interface and samples the real window
|
|
4
|
+
# rectangle from /health while the pet is moving, so we can prove:
|
|
5
|
+
# 1) the window actually travels a meaningful distance
|
|
6
|
+
# 2) the window size never drifts (the mixed-DPI ratchet regression)
|
|
7
|
+
# 3) the pet ends up back on the "ground line" (bottom of the work area)
|
|
8
|
+
#
|
|
9
|
+
# NOTE: keep this file pure ASCII. PowerShell 5.1 reads .ps1 as ANSI, so any
|
|
10
|
+
# non-ASCII comment breaks the parser (see README "known pitfalls").
|
|
11
|
+
|
|
12
|
+
param(
|
|
13
|
+
[string]$Mode = 'run',
|
|
14
|
+
[int]$Samples = 14,
|
|
15
|
+
[int]$IntervalMs = 200,
|
|
16
|
+
[int]$MinTravel = 90,
|
|
17
|
+
[int]$SizeSlack = 4
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
$ErrorActionPreference = 'Stop'
|
|
21
|
+
$base = 'http://127.0.0.1:8520'
|
|
22
|
+
|
|
23
|
+
function Get-Health {
|
|
24
|
+
$r = Invoke-RestMethod -Uri "$base/health" -TimeoutSec 5
|
|
25
|
+
return $r
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Post-Json([string]$path, [string]$body) {
|
|
29
|
+
return Invoke-RestMethod -Uri "$base$path" -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 5
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Write-Host "== desktop-pet roam probe =="
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
$h = Get-Health
|
|
36
|
+
} catch {
|
|
37
|
+
Write-Host "RESULT: FAIL - HTTP interface not reachable at $base (is the pet running?)"
|
|
38
|
+
exit 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Write-Host ("hwnd={0} scale={1}" -f $h.hwnd, $h.scale)
|
|
42
|
+
|
|
43
|
+
# Take manual control: the autonomous scheduler would fight us otherwise.
|
|
44
|
+
Post-Json '/auto' '{"enabled":false}' | Out-Null
|
|
45
|
+
Post-Json '/halt' '{}' | Out-Null
|
|
46
|
+
Start-Sleep -Milliseconds 600
|
|
47
|
+
|
|
48
|
+
$start = Get-Health
|
|
49
|
+
$sw = $start.bounds.width
|
|
50
|
+
$sh = $start.bounds.height
|
|
51
|
+
$sx = $start.bounds.x
|
|
52
|
+
$sy = $start.bounds.y
|
|
53
|
+
Write-Host ("start : x={0} y={1} size={2}x{3}" -f $sx, $sy, $sw, $sh)
|
|
54
|
+
|
|
55
|
+
Post-Json '/roam' ("{""mode"":""$Mode""}") | Out-Null
|
|
56
|
+
|
|
57
|
+
$xs = @()
|
|
58
|
+
$sizes = @()
|
|
59
|
+
for ($i = 0; $i -lt $Samples; $i++) {
|
|
60
|
+
Start-Sleep -Milliseconds $IntervalMs
|
|
61
|
+
try {
|
|
62
|
+
$b = (Get-Health).bounds
|
|
63
|
+
} catch {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
$xs += $b.x
|
|
67
|
+
$sizes += ("{0}x{1}" -f $b.width, $b.height)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
$end = (Get-Health).bounds
|
|
71
|
+
$minX = ($xs | Measure-Object -Minimum).Minimum
|
|
72
|
+
$maxX = ($xs | Measure-Object -Maximum).Maximum
|
|
73
|
+
$travel = [Math]::Abs($end.x - $sx)
|
|
74
|
+
$span = $maxX - $minX
|
|
75
|
+
$badSize = @($sizes | Where-Object { $_ -ne "$sw`x$sh" })
|
|
76
|
+
$maxW = ($sizes | ForEach-Object { [int]($_ -split 'x')[0] } | Measure-Object -Maximum).Maximum
|
|
77
|
+
$minW = ($sizes | ForEach-Object { [int]($_ -split 'x')[0] } | Measure-Object -Minimum).Minimum
|
|
78
|
+
$maxH = ($sizes | ForEach-Object { [int]($_ -split 'x')[1] } | Measure-Object -Maximum).Maximum
|
|
79
|
+
$minH = ($sizes | ForEach-Object { [int]($_ -split 'x')[1] } | Measure-Object -Minimum).Minimum
|
|
80
|
+
$drift = [Math]::Max($maxW - $sw, [Math]::Max($sw - $minW, [Math]::Max($maxH - $sh, $sh - $minH)))
|
|
81
|
+
|
|
82
|
+
Write-Host ("end : x={0} y={1} size={2}x{3}" -f $end.x, $end.y, $end.width, $end.height)
|
|
83
|
+
Write-Host ("travel: {0} px (samples moved across {1} px, min={2} max={3})" -f $travel, $span, $minX, $maxX)
|
|
84
|
+
Write-Host ("size : drift={0} px (slack {1}), distinct={2}" -f $drift, $SizeSlack, (($sizes | Select-Object -Unique) -join ','))
|
|
85
|
+
|
|
86
|
+
if ($drift -gt $SizeSlack) {
|
|
87
|
+
Write-Host "RESULT: FAIL - window size drifted while roaming"
|
|
88
|
+
exit 1
|
|
89
|
+
}
|
|
90
|
+
if ($travel -lt $MinTravel) {
|
|
91
|
+
Write-Host "RESULT: FAIL - window did not move (travel=$travel, expected >= $MinTravel)"
|
|
92
|
+
exit 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Write-Host "RESULT: PASS - roaming works"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# End-to-end drag test: locate the pet window by the HWND the app itself reports,
|
|
2
|
+
# press on the character body, drag, and compare window rect before/after.
|
|
3
|
+
# NOTE: ASCII only - Windows PowerShell 5.1 reads .ps1 as ANSI, CJK literals break parsing.
|
|
4
|
+
param([string]$Hwnd)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
$root = Split-Path -Parent $PSScriptRoot
|
|
8
|
+
$report = Join-Path $root 'sim-drag-report.txt'
|
|
9
|
+
$out = New-Object System.Collections.Generic.List[string]
|
|
10
|
+
function Say($t) { $out.Add($t) | Out-Null; Write-Host $t }
|
|
11
|
+
function Cursor() { $p = New-Object PetWin+RECTpt; [void][PetWin]::GetCursorPos([ref]$p); return "$($p.X),$($p.Y)" }
|
|
12
|
+
|
|
13
|
+
Add-Type @"
|
|
14
|
+
using System;
|
|
15
|
+
using System.Runtime.InteropServices;
|
|
16
|
+
public class PetWin {
|
|
17
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
18
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
|
|
19
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
20
|
+
[DllImport("user32.dll")] public static extern bool GetCursorPos(out RECTpt p);
|
|
21
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
|
|
22
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
23
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
|
24
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECTpt { public int X; public int Y; }
|
|
25
|
+
}
|
|
26
|
+
"@
|
|
27
|
+
|
|
28
|
+
# PowerShell 默认 DPI-unaware,坐标会和 Chromium 错位,必须先声明 DPI 感知
|
|
29
|
+
[void][PetWin]::SetProcessDPIAware()
|
|
30
|
+
|
|
31
|
+
$MOVE = 0x0001
|
|
32
|
+
$LDOWN = 0x0002
|
|
33
|
+
$LUP = 0x0004
|
|
34
|
+
|
|
35
|
+
if (-not $Hwnd) {
|
|
36
|
+
# Live source of truth first: the running app reports its own handle. The debug
|
|
37
|
+
# log can be stale (it survives restarts), so it is only a fallback.
|
|
38
|
+
try {
|
|
39
|
+
$health = Invoke-RestMethod -Uri 'http://127.0.0.1:8520/health' -TimeoutSec 3
|
|
40
|
+
if ($health.hwnd) { $Hwnd = [string]$health.hwnd }
|
|
41
|
+
} catch { }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (-not $Hwnd) {
|
|
45
|
+
$log = Join-Path $root 'pet-debug.log'
|
|
46
|
+
if (Test-Path $log) {
|
|
47
|
+
$m = Select-String -Path $log -Pattern 'hwnd=(\d+)' | Select-Object -Last 1
|
|
48
|
+
if ($m) { $Hwnd = $m.Matches[0].Groups[1].Value }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (-not $Hwnd) { Say 'FAIL: no hwnd (start the app, or pass -Hwnd)'; $out | Set-Content -Encoding ASCII $report; exit 1 }
|
|
53
|
+
|
|
54
|
+
$h = [IntPtr][long]$Hwnd
|
|
55
|
+
Say ("HWND={0} visible={1}" -f $Hwnd, [PetWin]::IsWindowVisible($h))
|
|
56
|
+
|
|
57
|
+
$r1 = New-Object PetWin+RECT
|
|
58
|
+
[bool]$okRect = [PetWin]::GetWindowRect($h, [ref]$r1)
|
|
59
|
+
Say ("GetWindowRect ok={0}" -f $okRect)
|
|
60
|
+
Say ("before: ({0},{1})-({2},{3}) size {4}x{5}" -f $r1.Left, $r1.Top, $r1.Right, $r1.Bottom, ($r1.Right - $r1.Left), ($r1.Bottom - $r1.Top))
|
|
61
|
+
|
|
62
|
+
# target = character body (hoodie), 50% / 58% inside the window
|
|
63
|
+
$startX = [int]($r1.Left + ($r1.Right - $r1.Left) * 0.50)
|
|
64
|
+
$startY = [int]($r1.Top + ($r1.Bottom - $r1.Top) * 0.58)
|
|
65
|
+
Say ("press point: ({0},{1})" -f $startX, $startY)
|
|
66
|
+
|
|
67
|
+
[void][PetWin]::SetCursorPos(30, 600)
|
|
68
|
+
Start-Sleep -Milliseconds 250
|
|
69
|
+
[void][PetWin]::SetCursorPos($startX, $startY)
|
|
70
|
+
Start-Sleep -Milliseconds 300
|
|
71
|
+
Say ("cursor after positioning: {0}" -f (Cursor))
|
|
72
|
+
[PetWin]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero)
|
|
73
|
+
Start-Sleep -Milliseconds 150
|
|
74
|
+
for ($i = 1; $i -le 12; $i++) {
|
|
75
|
+
[PetWin]::mouse_event($MOVE, -12, 7, 0, [IntPtr]::Zero)
|
|
76
|
+
Start-Sleep -Milliseconds 30
|
|
77
|
+
}
|
|
78
|
+
Say ("cursor after drag moves: {0} (dragged left/down on purpose)" -f (Cursor))
|
|
79
|
+
Start-Sleep -Milliseconds 200
|
|
80
|
+
[PetWin]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
|
|
81
|
+
Start-Sleep -Milliseconds 500
|
|
82
|
+
|
|
83
|
+
$r2 = New-Object PetWin+RECT
|
|
84
|
+
[void][PetWin]::GetWindowRect($h, [ref]$r2)
|
|
85
|
+
$dx = $r2.Left - $r1.Left
|
|
86
|
+
$dy = $r2.Top - $r1.Top
|
|
87
|
+
Say ("after : ({0},{1}) delta=({2},{3})" -f $r2.Left, $r2.Top, $dx, $dy)
|
|
88
|
+
|
|
89
|
+
# extra: single click, to check whether pointer events reach the renderer at all
|
|
90
|
+
# recompute from the CURRENT rect - the drag moved the window
|
|
91
|
+
$r3 = New-Object PetWin+RECT
|
|
92
|
+
[void][PetWin]::GetWindowRect($h, [ref]$r3)
|
|
93
|
+
$clickX = [int]($r3.Left + ($r3.Right - $r3.Left) * 0.50)
|
|
94
|
+
$clickY = [int]($r3.Top + ($r3.Bottom - $r3.Top) * 0.58)
|
|
95
|
+
[void][PetWin]::SetCursorPos(30, 600)
|
|
96
|
+
Start-Sleep -Milliseconds 250
|
|
97
|
+
[void][PetWin]::SetCursorPos($clickX, $clickY)
|
|
98
|
+
Start-Sleep -Milliseconds 250
|
|
99
|
+
[PetWin]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero)
|
|
100
|
+
Start-Sleep -Milliseconds 60
|
|
101
|
+
[PetWin]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
|
|
102
|
+
Start-Sleep -Milliseconds 400
|
|
103
|
+
Say 'click test done'
|
|
104
|
+
|
|
105
|
+
if ([Math]::Abs($dx) + [Math]::Abs($dy) -lt 20) { Say 'RESULT: FAIL - window did not move' } else { Say 'RESULT: PASS - drag works' }
|
|
106
|
+
|
|
107
|
+
$out | Set-Content -Encoding ASCII $report
|