Skip to content

Commit 64dbfb7

Browse files
committed
implement GetBuildEvents
1 parent be57f40 commit 64dbfb7

File tree

7 files changed

+486
-2
lines changed

7 files changed

+486
-2
lines changed

src/dsstats.maui/dsstats.builder/dsstats.builder/BuildArea.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@ public class BuildArea
1717
];
1818
private Dictionary<string, HashSet<RlPoint>> units = [];
1919

20+
public List<InputEvent> GetBuildEvents(ScreenArea screenArea)
21+
{
22+
// hardcoded unitmap for now
23+
Dictionary<string, char> UnitNameBuildMap = new()
24+
{
25+
{ "Zergling", 'q' },
26+
{ "Baneling", 'w' }
27+
};
28+
29+
List<InputEvent> events = [];
30+
var allUnits = units.SelectMany(unit =>
31+
unit.Value.Select(pos => new { UnitName = unit.Key, Position = pos }))
32+
.ToList();
33+
34+
if (allUnits.Count == 0)
35+
{
36+
return events;
37+
}
38+
39+
// NOTE: This is a nearest-neighbor search, which is O(n^2).
40+
// For a very large number of units, a more advanced spatial data structure
41+
// like a k-d tree or a quadtree would be more performant.
42+
var currentPos = GetCenter(); // Start from the center of the build area
43+
while (allUnits.Count != 0)
44+
{
45+
var nearest = allUnits.OrderBy(u => u.Position.DistanceTo(currentPos)).First();
46+
allUnits.Remove(nearest);
47+
currentPos = nearest.Position;
48+
49+
var unitChar = UnitNameBuildMap[nearest.UnitName];
50+
var screenPos = screenArea.GetScreenPosition(nearest.Position);
51+
52+
if (screenPos.Y < 15 || screenPos.Y > 1140) continue; // Skip scrolling for now
53+
54+
events.AddRange(DsBuilder.BuildUnit(unitChar, screenPos.X, screenPos.Y));
55+
}
56+
57+
return events;
58+
}
59+
2060
public bool PlaceUnit(string unit, RlPoint position)
2161
{
2262
if (!IsPointInsideOrOnEdge(position))
@@ -27,10 +67,39 @@ public bool PlaceUnit(string unit, RlPoint position)
2767
{
2868
unitPositions = units[unit] = [];
2969
}
30-
unitPositions.Add(position);
70+
unitPositions.Add(NormalizeToTop(position));
3171
return true;
3272
}
3373

74+
public void PlaceUnits(string unit, string positions, int team)
75+
{
76+
if (string.IsNullOrEmpty(positions))
77+
{
78+
return;
79+
}
80+
if (!units.TryGetValue(unit, out var unitPositions) || unitPositions == null)
81+
{
82+
unitPositions = units[unit] = [];
83+
}
84+
var newUnitPositions = GetUnitPositions(positions, team);
85+
foreach (var pos in newUnitPositions)
86+
{
87+
unitPositions.Add(pos);
88+
}
89+
}
90+
91+
private List<RlPoint> GetUnitPositions(string unitString, int team)
92+
{
93+
var stringPoints = unitString.Split(',', StringSplitOptions.RemoveEmptyEntries);
94+
List<RlPoint> mapPoints = [];
95+
for (int i = 0; i < stringPoints.Length; i += 2)
96+
{
97+
RlPoint mapPoint = new(int.Parse(stringPoints[i]), int.Parse(stringPoints[i + 1]));
98+
mapPoints.Add(NormalizeToTop(mapPoint));
99+
}
100+
return mapPoints;
101+
}
102+
34103
public RlPoint NormalizeToTop(RlPoint point)
35104
{
36105
var top = polygon[1]; // Top corner is reference
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
namespace dsstats.builder;
2+
3+
public static class BuildPlayer
4+
{
5+
public static void ReplayInput(List<InputEvent> events)
6+
{
7+
Console.WriteLine("Replaying events...");
8+
9+
bool shiftCurrentlyDown = false;
10+
bool mouseCurrentlyDown = false;
11+
bool ctrlCurrentlyDown = false;
12+
bool mouseMiddleCurrentlyDown = false;
13+
14+
foreach (var e in events)
15+
{
16+
Thread.Sleep(e.DelayMs);
17+
18+
// --- Handle Shift state ---
19+
if (e.ShiftKeyDown && !shiftCurrentlyDown)
20+
{
21+
User32Wrapper.keybd_event(User32Wrapper.VK_SHIFT, 0, 0, UIntPtr.Zero);
22+
shiftCurrentlyDown = true;
23+
}
24+
else if (!e.ShiftKeyDown && shiftCurrentlyDown)
25+
{
26+
User32Wrapper.keybd_event(User32Wrapper.VK_SHIFT, 0, User32Wrapper.KEYEVENTF_KEYUP, UIntPtr.Zero);
27+
shiftCurrentlyDown = false;
28+
}
29+
30+
// --- Handle Ctrl state ---
31+
if (e.CtrlKeyDown && !ctrlCurrentlyDown)
32+
{
33+
User32Wrapper.keybd_event(User32Wrapper.VK_CONTROL, 0, 0, UIntPtr.Zero);
34+
ctrlCurrentlyDown = true;
35+
}
36+
else if (!e.CtrlKeyDown && ctrlCurrentlyDown)
37+
{
38+
User32Wrapper.keybd_event(User32Wrapper.VK_CONTROL, 0, User32Wrapper.KEYEVENTF_KEYUP, UIntPtr.Zero);
39+
ctrlCurrentlyDown = false;
40+
}
41+
42+
// --- Handle Left Mouse Button state ---
43+
if (e.LeftMouseDown && !mouseCurrentlyDown)
44+
{
45+
User32Wrapper.mouse_event(User32Wrapper.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, UIntPtr.Zero);
46+
mouseCurrentlyDown = true;
47+
}
48+
else if (!e.LeftMouseDown && mouseCurrentlyDown)
49+
{
50+
User32Wrapper.mouse_event(User32Wrapper.MOUSEEVENTF_LEFTUP, 0, 0, 0, UIntPtr.Zero);
51+
mouseCurrentlyDown = false;
52+
}
53+
54+
if (e.MiddleMouseDown && !mouseMiddleCurrentlyDown)
55+
{
56+
User32Wrapper.mouse_event(User32Wrapper.MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, UIntPtr.Zero);
57+
mouseMiddleCurrentlyDown = true;
58+
}
59+
else if (!e.MiddleMouseDown && mouseMiddleCurrentlyDown)
60+
{
61+
User32Wrapper.mouse_event(User32Wrapper.MOUSEEVENTF_MIDDLEUP, 0, 0, 0, UIntPtr.Zero);
62+
mouseMiddleCurrentlyDown = false;
63+
}
64+
65+
if (e.Type == InputType.MouseClick)
66+
{
67+
// Move mouse (optional)
68+
User32Wrapper.SetCursorPos(e.X, e.Y);
69+
if (!e.LeftMouseDown)
70+
{
71+
User32Wrapper.mouse_event(User32Wrapper.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, UIntPtr.Zero);
72+
User32Wrapper.mouse_event(User32Wrapper.MOUSEEVENTF_LEFTUP, 0, 0, 0, UIntPtr.Zero);
73+
}
74+
}
75+
else if (e.Type == InputType.KeyPress)
76+
{
77+
User32Wrapper.keybd_event((byte)e.KeyCode, 0, 0, UIntPtr.Zero); // Key down
78+
User32Wrapper.keybd_event((byte)e.KeyCode, 0, User32Wrapper.KEYEVENTF_KEYUP, UIntPtr.Zero); // Key up
79+
}
80+
else if (e.Type == InputType.MouseMove)
81+
{
82+
User32Wrapper.SetCursorPos(e.X, e.Y);
83+
}
84+
else if (e.Type == InputType.MouseMoveRelative)
85+
{
86+
User32Wrapper.SimulateRelativeMouseMove(e.X, e.Y);
87+
}
88+
}
89+
if (shiftCurrentlyDown)
90+
User32Wrapper.keybd_event(User32Wrapper.VK_SHIFT, 0, User32Wrapper.KEYEVENTF_KEYUP, UIntPtr.Zero);
91+
92+
if (mouseCurrentlyDown)
93+
User32Wrapper.mouse_event(User32Wrapper.MOUSEEVENTF_LEFTUP, 0, 0, 0, UIntPtr.Zero);
94+
95+
96+
Console.WriteLine("Replay complete.");
97+
}
98+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
namespace dsstats.builder;
2+
3+
public static class DsBuilder
4+
{
5+
const int DelayMs = 5;
6+
7+
public static List<InputEvent> BuildArmy(string commander, int screenWidth, int screenHeight, bool dry = true)
8+
{
9+
List<InputEvent> events = [];
10+
11+
if (!dry)
12+
{
13+
// zoom out
14+
events.Add(new(InputType.KeyPress, 0, 0, User32Wrapper.VK_PRIOR, DelayMs));
15+
events.Add(new(InputType.KeyPress, 0, 0, User32Wrapper.VK_PRIOR, DelayMs));
16+
events.Add(new(InputType.KeyPress, 0, 0, User32Wrapper.VK_PRIOR, DelayMs));
17+
events.Add(new(InputType.KeyPress, 0, 0, User32Wrapper.VK_PRIOR, DelayMs));
18+
events.Add(new(InputType.KeyPress, 0, 0, User32Wrapper.VK_PRIOR, DelayMs));
19+
20+
// center worker
21+
events.Add(new(InputType.KeyPress, 0, 0, 0x31, DelayMs));
22+
events.AddRange(ScrollCenter());
23+
24+
// setup
25+
events.AddRange(EnterString("Infinite"));
26+
events.AddRange(EnterString("Clear"));
27+
events.AddRange(EnterString($"Enemy {commander}"));
28+
29+
// rebind worker
30+
events.Add(new(InputType.MouseClick, screenWidth / 2, screenHeight / 2, 0, DelayMs)); // TODO
31+
events.Add(new(InputType.KeyPress, 0, 0, 0x32, DelayMs, false, false, true));
32+
}
33+
34+
events.Add(new(InputType.KeyPress, 0, 0, 0x51, DelayMs));
35+
// build zerglings
36+
events.AddRange(BuildTestUnits(screenWidth, screenHeight));
37+
38+
return events;
39+
}
40+
41+
private static List<InputEvent> BuildTestUnits(int screenWidth, int screenHeight)
42+
{
43+
// y < 15 -> need to scroll top
44+
// y > 1140 -> need to scroll bottom
45+
46+
// team 2
47+
// Left: 73,82 Right: 101,76 Bottom: 90,65 Top: 84,93
48+
49+
int team = 2;
50+
string swarmlings = "90,75,90,82,83,82,83,75,84,75,85,75,86,75,87,75,88,75,89,75,90,76,90,77,90,78,90,79,90,80,90,81,89,82,88,82,87,82,86,82,85,82,84,82,83,81,83,80,83,79,83,78,83,77,83,76,91,83,91,82,91,81,91,80,91,79,91,78,91,77,91,76,91,75,91,74,90,74,89,74,88,74,87,74,86,74,85,74,84,74,83,74,82,74,82,75,82,76,82,77,82,78,82,79,82,80,82,81,82,82,82,83,83,83,84,83,85,83,86,83,87,83,88,83,89,83,90,83,81,74,81,75,81,76,81,77,81,78,81,79,81,80,81,81,81,82,81,83,81,84,82,84,83,84,84,84,85,84,86,84,87,84,88,84,89,84,90,84,91,84,92,84,92,83,92,82,92,81,92,80,92,79,92,78,92,77,92,76,92,75,92,74,92,73,91,73,90,73,89,73,88,73,87,73,86,73,85,73,84,73,83,73,82,73,83,72,84,72,85,72,86,72,87,72,88,72,89,72,90,72,91,72,92,72,93,72,93,73,93,74,93,75,93,76,93,77,93,78,93,79,93,80,93,81,93,82,93,83,93,84,92,85,91,85,90,85,89,85,88,85,87,85,86,85,85,85,84,85,83,85,82,85,81,85,80,85,80,84,80,83,80,82,80,81,80,80,80,79,80,78,80,77,80,76,80,75,79,76,79,77,79,78,79,79,79,80,79,81,79,82,79,83,79,84,79,85,79,86,80,86,81,86,82,86,83,86,84,86,85,86,86,86,87,86,88,86,89,86,90,86,91,86,94,83,94,82,94,81,94,80,94,79,94,78,94,77,94,76,94,75,94,74,94,73,94,72,94,71,93,71,92,71,91,71,90,71,89,71,88,71,87,71,86,71,84,71,85,71,85,70,86,70,87,70,88,70,89,70,90,70,91,70,92,70,93,70,94,70,95,70,95,71,95,72,96,73,95,73,95,74,95,75,95,76,95,77,95,79,95,78,95,80,95,81,95,82,96,81,96,80,96,71,97,72,96,72,98,73,97,73,99,74,98,74,97,74,96,74,96,75,97,75,98,75,99,75,100,75,96,76,97,76,98,76,99,76,100,76,101,76,100,77,99,77,98,77,97,77,96,77,96,78,96,79,97,80,98,79,99,78,98,78,97,78,97,79,86,69,87,68,87,69,88,68,88,67,88,69,89,69,89,68,89,67,89,66,90,65,90,66,91,66,90,67,91,67,92,67,93,68,92,68,91,68,90,68,94,69,93,69,92,69,91,69,90,69,78,77,77,78,78,78,78,79,77,79,76,79,75,80,76,80,77,80,78,80,78,81,77,81,76,81,75,81,74,81,78,82,77,82,76,82,75,82,74,82,73,82,74,83,75,83,76,83,77,83,78,83,78,84,77,84,76,84,75,84,76,85,77,85,78,85,78,86,77,86,78,87,79,87,80,87,79,88,80,88,81,87,82,87,80,89,81,88,81,89,82,88,83,87,84,87,83,88,83,89,82,89,81,90,82,90,82,91,83,90,84,89,84,88,85,88,85,87,86,87,87,87,88,87,89,87,90,87,89,88,88,88,87,88,86,88,88,89,87,90,87,89,86,91,86,90,86,89,85,89,85,90,84,90,83,91,83,92,84,93,84,92,84,91,85,91,85,92";
51+
string banelings = "88,77,88,80,85,80,85,77,84,76,89,81,89,76,84,81,86,76,87,76,84,78,84,79,86,81,87,81,89,79,89,78,88,76,87,77,86,77,85,76,89,77,84,77,85,78,87,80,88,78,88,79,85,79,86,80,85,81,89,80,88,81,84,80";
52+
53+
HashSet<RlPoint> rlPoints = [];
54+
55+
var buildArea = new BuildArea();
56+
buildArea.PlaceUnits("Zergling", swarmlings, team);
57+
buildArea.PlaceUnits("Baneling", banelings, team);
58+
59+
var screenArea = new ScreenArea(2560, 1440);
60+
var events = buildArea.GetBuildEvents(screenArea);
61+
62+
return events;
63+
}
64+
65+
private static List<InputEvent> ScrollCenter(int key = 0x31)
66+
{
67+
List<InputEvent> events = [];
68+
events.Add(new(InputType.KeyPress, 0, 0, key, DelayMs));
69+
events.Add(new(InputType.KeyPress, 0, 0, key, DelayMs));
70+
return events;
71+
}
72+
73+
public static List<InputEvent> BuildUnit(char c, int x, int y)
74+
{
75+
List<InputEvent> events = [];
76+
if (User32Wrapper.TryMapCharToKey(c, out var vkCode, out var requiresShift))
77+
{
78+
events.Add(new(InputType.MouseMove, x, y, 0, DelayMs));
79+
events.Add(new(InputType.KeyPress, 0, 0, vkCode, DelayMs, requiresShift));
80+
events.Add(new(InputType.MouseClick, x, y, 0, DelayMs));
81+
}
82+
return events;
83+
}
84+
85+
private static List<InputEvent> ScrollY(int offsetY, RlPoint center)
86+
{
87+
List<InputEvent> events = [];
88+
events.Add(new(InputType.MouseMove, center.X, center.Y, 0, DelayMs));
89+
90+
int step = 2; // You can adjust this for finer or coarser steps
91+
int remaining = Math.Abs(offsetY);
92+
int direction = Math.Sign(offsetY);
93+
94+
while (remaining > 0)
95+
{
96+
int delta = Math.Min(step, remaining) * direction;
97+
events.Add(new(InputType.MouseMoveRelative, 0, delta, 0, 1, false, false, false, true));
98+
remaining -= Math.Abs(delta);
99+
}
100+
101+
events.Add(new(InputType.MouseMove, center.X, center.Y, 0, DelayMs));
102+
return events;
103+
}
104+
105+
private static List<InputEvent> EnterString(string msg)
106+
{
107+
List<InputEvent> events = [];
108+
events.Add(new(InputType.KeyPress, 0, 0, User32Wrapper.VK_RETURN, DelayMs));
109+
for (int i = 0; i < msg.Length; i++)
110+
{
111+
var c = msg[i];
112+
if (User32Wrapper.TryMapCharToKey(c, out int vk, out bool requiresShift))
113+
{
114+
events.Add(new InputEvent(InputType.KeyPress, 0, 0, vk, DelayMs, requiresShift, false));
115+
}
116+
}
117+
events.Add(new(InputType.KeyPress, 0, 0, User32Wrapper.VK_RETURN, DelayMs));
118+
return events;
119+
}
120+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace dsstats.builder;
2+
3+
public sealed record InputEvent(InputType Type, int X, int Y, int KeyCode, int DelayMs,
4+
bool ShiftKeyDown = false, bool LeftMouseDown = false, bool CtrlKeyDown = false, bool MiddleMouseDown = false);
5+
6+
public enum InputType
7+
{
8+
None = 0,
9+
MouseClick = 1,
10+
MouseMove = 2,
11+
KeyPress = 3,
12+
MouseMoveRelative = 4
13+
}

src/dsstats.maui/dsstats.builder/dsstats.builder/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ class Program
44
{
55
static void Main(string[] args)
66
{
7-
Console.WriteLine("Hello, World!");
7+
Thread.Sleep(1000);
8+
var events = DsBuilder.BuildArmy("Zerg", 2560, 1440, false);
9+
BuildPlayer.ReplayInput(events);
810
}
911
}

src/dsstats.maui/dsstats.builder/dsstats.builder/RlPoint.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ namespace dsstats.builder;
22

33
public sealed record RlPoint(int X, int Y)
44
{
5+
public double DistanceTo(RlPoint other)
6+
{
7+
return Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
8+
}
9+
510
public static RlPoint Lerp(RlPoint a, RlPoint b, double t)
611
{
712
return new RlPoint(

0 commit comments

Comments
 (0)