Skip to content

Commit 7e5535f

Browse files
committed
CmdrBuilds / Readme
1 parent 64dbfb7 commit 7e5535f

File tree

9 files changed

+362
-37
lines changed

9 files changed

+362
-37
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
# dsstats builder
3+
4+
A tool to automatically build units in the Direct Strike Tutorial map (StarCraft II) based on replay data.
5+
6+
## Features
7+
* Automatically places units according to replay information
8+
* Uses simulated keyboard and mouse input
9+
* Helps recreate builds for training or analysis
10+
11+
## Getting Started
12+
1. **Close all other applications.**
13+
2. Launch the Direct Strike Tutorial map in StarCraft II.
14+
3. Assign hotkeys:
15+
* Set Team 1's worker (top player) to hotkey 1
16+
* Set Team 2's worker (bottom player) to hotkey 2
17+
4. **Do not move the workers!**
18+
* For accurate unit placement, workers must remain at the center of their designated build zones.
19+
5. Open dsstats, load the replay you want to test, open the desired build, and click "Build". (Screenshot TODO)
20+
6. Switch back to StarCraft II and do not touch your mouse or keyboard until the build is complete.
21+
7. Ensure no other application is in focus, as dsstats sends automated mouse and keyboard inputs that may interfere with active windows.
22+
23+
## How It Works
24+
* Parses replay files to extract unit placement data
25+
* Maps unit positions to screen coordinates
26+
* Simulates precise keyboard and mouse inputs to replicate builds
27+
28+
## TODO
29+
* top map build area corners
30+
* check terran build
31+
* check protoss build
32+
* build hidden units (Lurker, Widowmine)
33+
* ability/upgrades
34+
* test other screen resolutions
35+
* team 2 build / test
36+
* Commanders builds

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

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,41 +17,79 @@ public class BuildArea
1717
];
1818
private Dictionary<string, HashSet<RlPoint>> units = [];
1919

20-
public List<InputEvent> GetBuildEvents(ScreenArea screenArea)
20+
public List<InputEvent> GetBuildEvents(ScreenArea screenArea, CmdrBuild build)
2121
{
22-
// hardcoded unitmap for now
23-
Dictionary<string, char> UnitNameBuildMap = new()
24-
{
25-
{ "Zergling", 'q' },
26-
{ "Baneling", 'w' }
27-
};
28-
2922
List<InputEvent> events = [];
3023
var allUnits = units.SelectMany(unit =>
31-
unit.Value.Select(pos => new { UnitName = unit.Key, Position = pos }))
24+
unit.Value
25+
.OrderBy(o => o.X).ThenBy(t => t.Y)
26+
.Select(pos => new BuildUnit(unit.Key, pos)))
3227
.ToList();
3328

3429
if (allUnits.Count == 0)
3530
{
3631
return events;
3732
}
3833

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;
34+
List<BuildUnit> topUnits = [];
35+
List<BuildUnit> centerUnits = [];
36+
List<BuildUnit> bottomUnits = [];
4837

49-
var unitChar = UnitNameBuildMap[nearest.UnitName];
50-
var screenPos = screenArea.GetScreenPosition(nearest.Position);
38+
foreach (var unit in allUnits)
39+
{
5140

52-
if (screenPos.Y < 15 || screenPos.Y > 1140) continue; // Skip scrolling for now
41+
var screenPos = screenArea.GetScreenPosition(unit.Pos);
42+
if (screenPos.Y <= 15)
43+
{
44+
topUnits.Add(new(unit.UnitName, screenPos));
45+
}
46+
else if (screenPos.Y >= 1140)
47+
{
48+
bottomUnits.Add(new(unit.UnitName, screenPos));
49+
}
50+
else
51+
{
52+
centerUnits.Add(new(unit.UnitName, screenPos));
53+
}
54+
}
5355

54-
events.AddRange(DsBuilder.BuildUnit(unitChar, screenPos.X, screenPos.Y));
56+
foreach (var centerUnit in centerUnits)
57+
{
58+
var unitChar = build.GetUnitChar(centerUnit.UnitName);
59+
if (unitChar is null)
60+
{
61+
continue;
62+
}
63+
events.AddRange(DsBuilder.BuildUnit(unitChar.Value, centerUnit.Pos.X, centerUnit.Pos.Y));
64+
}
65+
if (topUnits.Count > 0)
66+
{
67+
events.AddRange(DsBuilder.ScrollY(Convert.ToInt32(250 * screenArea._scaleY), screenArea.GetCenter()));
68+
foreach (var topUnit in topUnits)
69+
{
70+
var unitChar = build.GetUnitChar(topUnit.UnitName);
71+
if (unitChar is null)
72+
{
73+
continue;
74+
}
75+
events.AddRange(DsBuilder.BuildUnit(unitChar.Value, topUnit.Pos.X, topUnit.Pos.Y + Convert.ToInt32(125 * screenArea._scaleY)));
76+
}
77+
}
78+
if (bottomUnits.Count > 0)
79+
{
80+
Console.WriteLine($"bottom units: {bottomUnits.Count}");
81+
events.AddRange(DsBuilder.ScrollCenter());
82+
events.Add(new(InputType.KeyPress, 0, 0, 0x51, 5)); // Build Menu
83+
events.AddRange(DsBuilder.ScrollY(Convert.ToInt32(-500 * screenArea._scaleY), screenArea.GetCenter()));
84+
foreach (var bottomUnit in bottomUnits)
85+
{
86+
var unitChar = build.GetUnitChar(bottomUnit.UnitName);
87+
if (unitChar is null)
88+
{
89+
continue;
90+
}
91+
events.AddRange(DsBuilder.BuildUnit(unitChar.Value, bottomUnit.Pos.X, bottomUnit.Pos.Y - Convert.ToInt32(300 * screenArea._scaleY)));
92+
}
5593
}
5694

5795
return events;
@@ -173,3 +211,4 @@ private static bool IsOnEdge(RlPoint p, RlPoint a, RlPoint b)
173211
}
174212
}
175213

214+
internal sealed record BuildUnit(string UnitName, RlPoint Pos);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using dsstats.shared;
2+
3+
namespace dsstats.builder;
4+
5+
public abstract class CmdrBuild
6+
{
7+
protected Dictionary<string, char> UnitMap = [];
8+
protected Dictionary<string, char> UpgradeMap = [];
9+
protected Dictionary<string, char> AbilityMap = [];
10+
11+
public virtual char? GetUnitChar(string unitName)
12+
{
13+
return UnitMap.TryGetValue(unitName, out var c)
14+
? c
15+
: null;
16+
}
17+
18+
public virtual char? GetUpgradeChar(string upgradeName)
19+
{
20+
return UpgradeMap.TryGetValue(upgradeName, out var c)
21+
? c
22+
: null;
23+
}
24+
25+
public virtual char? GetAbilityChar(string abilityName)
26+
{
27+
return AbilityMap.TryGetValue(abilityName, out var c)
28+
? c
29+
: null;
30+
}
31+
}
32+
33+
public static class CmdrBuildFactory
34+
{
35+
public static CmdrBuild? Create(Commander commander)
36+
{
37+
return commander switch
38+
{
39+
Commander.Protoss => new ProtossBuild(),
40+
Commander.Terran => new TerranBuild(),
41+
Commander.Zerg => new ZergBuild(),
42+
_ => null
43+
};
44+
}
45+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace dsstats.builder;
2+
3+
public class ProtossBuild : CmdrBuild
4+
{
5+
public ProtossBuild()
6+
{
7+
UnitMap = new Dictionary<string, char>
8+
{
9+
// Q-menu units
10+
{ "Zealot", 'q' },
11+
{ "Stalker", 'w' },
12+
{ "Adept", 'e' },
13+
{ "Sentry", 'r' },
14+
{ "HighTemplar", 't' },
15+
{ "DarkTemplar", 'a' },
16+
{ "Immortal", 's' },
17+
{ "Colossus", 'd' },
18+
{ "Disruptor", 'f' },
19+
{ "Archon", 'g' },
20+
{ "Phoenix", 'z' },
21+
{ "VoidRay", 'x' },
22+
{ "Oracle", 'c' },
23+
{ "Tempest", 'v' },
24+
{ "Carrier", 'b' },
25+
{ "Mothership", 'n' },
26+
};
27+
28+
AbilityMap = new Dictionary<string, char>
29+
{
30+
// W-menu: abilities and tech
31+
{ "Charge", 'q' },
32+
{ "Blink", 'w' },
33+
{ "ResonatingGlaives", 'e' },
34+
{ "GuardianShield", 'r' },
35+
{ "PsionicStorm", 't' },
36+
{ "ShadowStride", 'a' },
37+
{ "Barrier", 's' },
38+
{ "ExtendedThermalLance", 'd' },
39+
{ "PurificationNova", 'f' },
40+
{ "GravitonBeam", 'g' },
41+
{ "FluxVanes", 'z' },
42+
{ "Revelation", 'x' },
43+
{ "TectonicDestabilizers", 'c' },
44+
{ "InterceptorLaunchSpeed", 'v' },
45+
{ "MassRecall", 'b' },
46+
};
47+
48+
UpgradeMap = new Dictionary<string, char>
49+
{
50+
// Forge + Cyber Core + Fleet Beacon
51+
{ "GroundWeaponsLevel1", 'a' },
52+
{ "GroundWeaponsLevel2", 'a' },
53+
{ "GroundWeaponsLevel3", 'a' },
54+
{ "GroundArmorLevel1", 's' },
55+
{ "GroundArmorLevel2", 's' },
56+
{ "GroundArmorLevel3", 's' },
57+
{ "ShieldsLevel1", 'd' },
58+
{ "ShieldsLevel2", 'd' },
59+
{ "ShieldsLevel3", 'd' },
60+
{ "AirWeaponsLevel1", 'f' },
61+
{ "AirWeaponsLevel2", 'f' },
62+
{ "AirWeaponsLevel3", 'f' },
63+
{ "AirArmorLevel1", 'g' },
64+
{ "AirArmorLevel2", 'g' },
65+
{ "AirArmorLevel3", 'g' },
66+
};
67+
}
68+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
namespace dsstats.builder;
2+
3+
public class TerranBuild : CmdrBuild
4+
{
5+
public TerranBuild()
6+
{
7+
UnitMap = new Dictionary<string, char>
8+
{
9+
// Q-menu
10+
{ "Marine", 'q' },
11+
{ "Marauder", 'w' },
12+
{ "Reaper", 'e' },
13+
{ "Ghost", 'r' },
14+
{ "Hellion", 't' },
15+
{ "Hellbat", 'a' },
16+
{ "SiegeTank", 's' },
17+
{ "WidowMine", 'd' },
18+
{ "Cyclone", 'f' },
19+
{ "Thor", 'g' },
20+
{ "Viking", 'z' },
21+
{ "Medivac", 'x' },
22+
{ "Banshee", 'c' },
23+
{ "Raven", 'v' },
24+
{ "Battlecruiser", 'b' },
25+
};
26+
27+
AbilityMap = new Dictionary<string, char>
28+
{
29+
// W-menu (upgrades & abilities)
30+
{ "Stimpack", 'q' },
31+
{ "ConcussiveShells", 'w' },
32+
{ "CloakingField", 'e' },
33+
{ "DrillingClaws", 'r' },
34+
{ "InfernalPreigniter", 't' }, // Hellion upgrade
35+
{ "SmartServos", 'a' }, // Viking/Thor transform speed
36+
{ "HighImpactPayload", 's' }, // Thor mode toggle
37+
{ "HyperflightRotors", 'd' }, // Banshee speed
38+
{ "AdvancedBallistics", 'f' }, // Raven range
39+
{ "WeaponRefit", 'g' }, // Battlecruiser Yamato
40+
};
41+
42+
UpgradeMap = new Dictionary<string, char>
43+
{
44+
// Main armory upgrades
45+
{ "InfantryWeaponsLevel1", 'a' },
46+
{ "InfantryWeaponsLevel2", 'a' },
47+
{ "InfantryWeaponsLevel3", 'a' },
48+
{ "InfantryArmorLevel1", 's' },
49+
{ "InfantryArmorLevel2", 's' },
50+
{ "InfantryArmorLevel3", 's' },
51+
{ "VehicleWeaponsLevel1", 'd' },
52+
{ "VehicleWeaponsLevel2", 'd' },
53+
{ "VehicleWeaponsLevel3", 'd' },
54+
{ "ShipWeaponsLevel1", 'f' },
55+
{ "ShipWeaponsLevel2", 'f' },
56+
{ "ShipWeaponsLevel3", 'f' },
57+
{ "VehiclePlatingLevel1", 'g' },
58+
{ "VehiclePlatingLevel2", 'g' },
59+
{ "VehiclePlatingLevel3", 'g' },
60+
{ "ShipPlatingLevel1", 'h' },
61+
{ "ShipPlatingLevel2", 'h' },
62+
{ "ShipPlatingLevel3", 'h' },
63+
};
64+
}
65+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace dsstats.builder;
2+
3+
public class ZergBuild : CmdrBuild
4+
{
5+
public ZergBuild()
6+
{
7+
UnitMap = new Dictionary<string, char>
8+
{
9+
{ "Zergling", 'q' },
10+
{ "Baneling", 'w' },
11+
{ "Roach", 'e' },
12+
{ "Queen", 'r' },
13+
{ "Overseer", 't' },
14+
{ "Hydralisk", 'a' },
15+
{ "Mutalisk", 's' },
16+
{ "Corruptor", 'd' },
17+
{ "Infestor", 'f' },
18+
{ "Swarmhost", 'g' },
19+
{ "Viper", 'z' },
20+
{ "Ultralisk", 'x' },
21+
{ "Broodloard", 'c' },
22+
};
23+
24+
AbilityMap = new Dictionary<string, char>
25+
{
26+
{ "MetabolikBoost", 'q' },
27+
{ "AdrenalGlands", 'w' },
28+
{ "CentrifugalHooks", 'e' },
29+
{ "GlialReconstitution", 'r' },
30+
{ "TunnelingClaws", 't' },
31+
{ "GroovedSpines", 'a' },
32+
{ "SeismicSpines", 's' },
33+
{ "AdaptiveTalons", 'd' },
34+
{ "NeuralParasite", 'g' },
35+
{ "ChitinousPlating", 'z' },
36+
{ "AnabolicSynthesis", 'x' },
37+
{ "MuscularAugments", 'c' },
38+
{ "PneumatizedCarapace", 'v' },
39+
};
40+
41+
UpgradeMap = new Dictionary<string, char>
42+
{
43+
{ "MeleeAttacksLevel1", 'a' },
44+
{ "MeleeAttacksLevel2", 'a' },
45+
{ "MeleeAttacksLevel3", 'a' },
46+
{ "GroundCarapaceLevel1", 's' },
47+
{ "GroundCarapaceLevel2", 's' },
48+
{ "GroundCarapaceLevel3", 's' },
49+
{ "MissileAttacksLevel1", 'd' },
50+
{ "MissileAttacksLevel2", 'd' },
51+
{ "MissileAttacksLevel3", 'd' },
52+
{ "FlyerAttacksLevel1", 'f' },
53+
{ "FlyerAttacksLevel2", 'f' },
54+
{ "FlyerAttacksLevel3", 'f' },
55+
{ "FlyerCarapaceLevel1", 'g' },
56+
{ "FlyerCarapaceLevel2", 'g' },
57+
{ "FlyerCarapaceLevel3", 'g' },
58+
};
59+
}
60+
}

0 commit comments

Comments
 (0)