diff --git a/src/TSMapEditor/Config/Default/UI/Windows/AITargetTypesWindow.ini b/src/TSMapEditor/Config/Default/UI/Windows/AITargetTypesWindow.ini
new file mode 100644
index 00000000..9c227219
--- /dev/null
+++ b/src/TSMapEditor/Config/Default/UI/Windows/AITargetTypesWindow.ini
@@ -0,0 +1,86 @@
+[AITargetTypesWindow]
+Width=920
+Height=560
+Enabled=yes
+$CC0=lblAITargetTypes:XNALabel
+$CC1=btnNewAITargetType:EditorButton
+$CC2=btnDeleteAITargetType:EditorButton
+$CC3=btnCloneAITargetType:EditorButton
+$CC4=lbAITargetTypes:EditorListBox
+$CC5=lblTechnoTypes:XNALabel
+$CC6=btnAddTechno:EditorButton
+$CC7=btnDeleteTechno:EditorButton
+$CC8=lbTechnoEntries:EditorListBox
+$CC9=lblAvailableTechnos:XNALabel
+$CC10=tbSearchTechno:EditorSuggestionTextBox
+$CC11=lbAvailableTechnos:EditorListBox
+HasCloseButton=yes
+
+[lblAITargetTypes]
+$X=EMPTY_SPACE_SIDES
+$Y=EMPTY_SPACE_TOP
+FontIndex=1
+$Text=translate(AITargetTypes:)
+
+[btnNewAITargetType]
+$X=EMPTY_SPACE_SIDES
+$Y=getBottom(lblAITargetTypes) + EMPTY_SPACE_TOP
+$Width=300
+$Text=translate(New AITargetType)
+
+[btnDeleteAITargetType]
+$X=EMPTY_SPACE_SIDES
+$Y=getBottom(btnNewAITargetType) + VERTICAL_SPACING
+$Width=getWidth(btnNewAITargetType)
+$Text=translate(Delete AITargetType)
+
+[lbAITargetTypes]
+$X=EMPTY_SPACE_SIDES
+$Y=getBottom(btnCloneAITargetType) + VERTICAL_SPACING
+$Width=getWidth(btnNewAITargetType)
+$Height=getHeight(AITargetTypesWindow) - getY(lbAITargetTypes) - EMPTY_SPACE_BOTTOM
+
+[lblTechnoTypes]
+$X=getRight(lbAITargetTypes) + (HORIZONTAL_SPACING * 2)
+$Y=getY(lblAITargetTypes)
+$Text=translate(TechnoTypes:)
+
+[btnAddTechno]
+$X=getX(lblTechnoTypes)
+$Y=getBottom(lblTechnoTypes) + EMPTY_SPACE_TOP
+$Width=220
+$Text=translate(Add Techno)
+
+[btnDeleteTechno]
+$X=getX(btnAddTechno)
+$Y=getBottom(btnAddTechno) + VERTICAL_SPACING
+$Width=getWidth(btnAddTechno)
+$Text=translate(Delete Techno)
+
+[btnCloneAITargetType]
+$X=EMPTY_SPACE_SIDES
+$Y=getBottom(btnDeleteAITargetType) + VERTICAL_SPACING
+$Width=getWidth(btnNewAITargetType)
+$Text=translate(Clone AITargetType)
+
+[lblAvailableTechnos]
+$X=getRight(lbTechnoEntries) + (HORIZONTAL_SPACING * 2)
+$Y=getY(lblTechnoTypes)
+$Text=translate(Available TechnoTypes:)
+
+[tbSearchTechno]
+$X=getX(lblAvailableTechnos)
+$Y=getBottom(lblAvailableTechnos) + EMPTY_SPACE_TOP
+$Width=getWidth(AITargetTypesWindow) - getX(tbSearchTechno) - EMPTY_SPACE_SIDES
+
+[lbAvailableTechnos]
+$X=getX(tbSearchTechno)
+$Y=getBottom(tbSearchTechno) + EMPTY_SPACE_TOP
+$Width=getWidth(tbSearchTechno)
+$Height=getHeight(AITargetTypesWindow) - getY(lbAvailableTechnos) - EMPTY_SPACE_BOTTOM
+
+[lbTechnoEntries]
+$X=getX(btnAddTechno)
+$Y=getBottom(btnDeleteTechno) + VERTICAL_SPACING
+$Width=getWidth(btnAddTechno)
+$Height=getHeight(AITargetTypesWindow) - getY(lbTechnoEntries) - EMPTY_SPACE_BOTTOM
diff --git a/src/TSMapEditor/TSMapEditor.csproj b/src/TSMapEditor/TSMapEditor.csproj
index 7392e84c..b3ad1e12 100644
--- a/src/TSMapEditor/TSMapEditor.csproj
+++ b/src/TSMapEditor/TSMapEditor.csproj
@@ -136,6 +136,9 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/src/TSMapEditor/UI/TopBar/TopBarMenu.cs b/src/TSMapEditor/UI/TopBar/TopBarMenu.cs
index 27d5713d..cf522cb9 100644
--- a/src/TSMapEditor/UI/TopBar/TopBarMenu.cs
+++ b/src/TSMapEditor/UI/TopBar/TopBarMenu.cs
@@ -243,6 +243,8 @@ public override void Initialize()
scriptingContextMenu.AddItem(Translate(this, "Scripting.TeamTypes", "TeamTypes"), () => windowController.TeamTypesWindow.Open(), null, null, null);
scriptingContextMenu.AddItem(Translate(this, "Scripting.LocalVariables", "Local Variables"), () => windowController.LocalVariablesWindow.Open(), null, null, null);
scriptingContextMenu.AddItem(Translate(this, "Scripting.AITriggers", "AITriggers"), () => windowController.AITriggersWindow.Open(), null, null, null, null);
+ if (Constants.IsRA2YR)
+ scriptingContextMenu.AddItem(Translate(this, "Scripting.AITargetTypes", "AITargetTypes"), () => windowController.AITargetTypesWindow.Open(), null, null, null);
var scriptingButton = new MenuButton(WindowManager, scriptingContextMenu);
scriptingButton.Name = nameof(scriptingButton);
diff --git a/src/TSMapEditor/UI/Windows/AITargetTypesWindow.cs b/src/TSMapEditor/UI/Windows/AITargetTypesWindow.cs
new file mode 100644
index 00000000..28673fed
--- /dev/null
+++ b/src/TSMapEditor/UI/Windows/AITargetTypesWindow.cs
@@ -0,0 +1,271 @@
+using Rampastring.XNAUI;
+using Rampastring.XNAUI.XNAControls;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TSMapEditor.Models;
+using TSMapEditor.UI;
+using TSMapEditor.UI.Controls;
+
+namespace TSMapEditor.UI.Windows
+{
+ public class AITargetTypesWindow : INItializableWindow
+ {
+ public AITargetTypesWindow(WindowManager windowManager, Map map) : base(windowManager)
+ {
+ this.map = map;
+ }
+
+ private readonly Map map;
+
+ private EditorListBox lbAITargetTypes;
+ private EditorListBox lbTechnoEntries;
+ private EditorListBox lbAvailableTechnos;
+ private EditorSuggestionTextBox tbSearchTechno;
+
+ private readonly List allAvailableTechnos = new List();
+ private int editedIndex = -1;
+
+ public override void Initialize()
+ {
+ Name = nameof(AITargetTypesWindow);
+ base.Initialize();
+
+ lbAITargetTypes = FindChild(nameof(lbAITargetTypes));
+ lbTechnoEntries = FindChild(nameof(lbTechnoEntries));
+ lbAvailableTechnos = FindChild(nameof(lbAvailableTechnos));
+ tbSearchTechno = FindChild(nameof(tbSearchTechno));
+
+ UIHelpers.AddSearchTipsBoxToControl(tbSearchTechno);
+
+ FindChild("btnNewAITargetType").LeftClick += BtnNewAITargetType_LeftClick;
+ FindChild("btnDeleteAITargetType").LeftClick += BtnDeleteAITargetType_LeftClick;
+ FindChild("btnCloneAITargetType").LeftClick += BtnCloneAITargetType_LeftClick;
+ FindChild("btnAddTechno").LeftClick += BtnAddTechno_LeftClick;
+ FindChild("btnDeleteTechno").LeftClick += BtnDeleteTechno_LeftClick;
+
+ lbAITargetTypes.SelectedIndexChanged += LbAITargetTypes_SelectedIndexChanged;
+ lbTechnoEntries.SelectedIndexChanged += (s, e) => { }; // used for delete button only
+
+ tbSearchTechno.TextChanged += (s, e) => ApplyTechnoFilter();
+
+ ListAvailableTechnos();
+ ListAITargetTypes();
+ }
+
+ public void Open()
+ {
+ ListAvailableTechnos();
+ ListAITargetTypes();
+ Show();
+ }
+
+ private void ListAITargetTypes()
+ {
+ lbAITargetTypes.Clear();
+
+ var section = map.LoadedINI.GetSection("AITargetTypes");
+ if (section == null)
+ return;
+
+ for (int i = 0; ; i++)
+ {
+ string value = section.GetStringValue(i.ToString(), string.Empty);
+ if (string.IsNullOrEmpty(value))
+ break;
+
+ lbAITargetTypes.AddItem(new XNAListBoxItem { Text = $"{i}: {value}", Tag = i });
+ }
+ }
+
+ private void LbAITargetTypes_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ if (lbAITargetTypes.SelectedItem == null)
+ {
+ editedIndex = -1;
+ lbTechnoEntries.Clear();
+ return;
+ }
+
+ editedIndex = (int)lbAITargetTypes.SelectedItem.Tag;
+
+ var section = map.LoadedINI.GetSection("AITargetTypes");
+ string value = section?.GetStringValue(editedIndex.ToString(), string.Empty) ?? string.Empty;
+ PopulateTechnoEntries(value);
+ }
+
+ private void PopulateTechnoEntries(string value)
+ {
+ lbTechnoEntries.Clear();
+ if (string.IsNullOrWhiteSpace(value))
+ return;
+
+ var tokens = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var token in tokens)
+ {
+ var trimmed = token.Trim();
+ var match = allAvailableTechnos.FirstOrDefault(t => string.Equals(t.ININame, trimmed, StringComparison.CurrentCultureIgnoreCase));
+ lbTechnoEntries.AddItem(new XNAListBoxItem { Text = trimmed, Tag = (object)match ?? trimmed });
+ }
+ }
+
+ private void ListAvailableTechnos()
+ {
+ allAvailableTechnos.Clear();
+ allAvailableTechnos.AddRange(map.Rules.UnitTypes);
+ allAvailableTechnos.AddRange(map.Rules.InfantryTypes);
+ allAvailableTechnos.AddRange(map.Rules.BuildingTypes);
+ allAvailableTechnos.AddRange(map.Rules.AircraftTypes);
+ allAvailableTechnos.Sort((a, b) => string.Compare(a.ININame, b.ININame, StringComparison.OrdinalIgnoreCase));
+
+ ApplyTechnoFilter();
+ }
+
+ private void ApplyTechnoFilter()
+ {
+ if (lbAvailableTechnos == null)
+ return;
+
+ lbAvailableTechnos.Clear();
+
+ string filter = tbSearchTechno?.Text?.Trim() ?? string.Empty;
+ bool showAll = string.IsNullOrWhiteSpace(filter) || filter.Equals(tbSearchTechno.Suggestion, StringComparison.CurrentCultureIgnoreCase);
+
+ foreach (var techno in allAvailableTechnos)
+ {
+ if (!showAll && !techno.ININame.Contains(filter, StringComparison.CurrentCultureIgnoreCase) && !techno.GetEditorDisplayName().Contains(filter, StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ lbAvailableTechnos.AddItem(new XNAListBoxItem { Text = $"{techno.ININame} ({techno.GetEditorDisplayName()})", Tag = techno });
+ }
+ }
+
+ private void BtnNewAITargetType_LeftClick(object sender, EventArgs e)
+ {
+ var section = map.LoadedINI.GetSection("AITargetTypes");
+ if (section == null)
+ {
+ map.LoadedINI.AddSection("AITargetTypes");
+ section = map.LoadedINI.GetSection("AITargetTypes");
+ }
+
+ int newIndex = 0;
+ while (!string.IsNullOrEmpty(section.GetStringValue(newIndex.ToString(), string.Empty)))
+ newIndex++;
+
+ section.SetStringValue(newIndex.ToString(), "E1");
+ ListAITargetTypes();
+ lbAITargetTypes.SelectedIndex = lbAITargetTypes.Items.Count - 1;
+ }
+
+ private void BtnDeleteAITargetType_LeftClick(object sender, EventArgs e)
+ {
+ if (editedIndex == -1)
+ return;
+
+ var section = map.LoadedINI.GetSection("AITargetTypes");
+ if (section == null)
+ return;
+
+ int current = editedIndex;
+ while (true)
+ {
+ string nextValue = section.GetStringValue((current + 1).ToString(), string.Empty);
+ if (string.IsNullOrEmpty(nextValue))
+ {
+ section.RemoveKey(current.ToString());
+ break;
+ }
+
+ section.SetStringValue(current.ToString(), nextValue);
+ current++;
+ }
+
+ ListAITargetTypes();
+ if (lbAITargetTypes.Items.Count == 0)
+ editedIndex = -1;
+ else
+ lbAITargetTypes.SelectedIndex = Math.Min(current, lbAITargetTypes.Items.Count - 1);
+ }
+
+ private void BtnCloneAITargetType_LeftClick(object sender, EventArgs e)
+ {
+ if (editedIndex == -1)
+ return;
+
+ var section = map.LoadedINI.GetSection("AITargetTypes");
+ if (section == null)
+ {
+ map.LoadedINI.AddSection("AITargetTypes");
+ section = map.LoadedINI.GetSection("AITargetTypes");
+ }
+
+ string value = section.GetStringValue(editedIndex.ToString(), string.Empty);
+ int newIndex = 0;
+ while (!string.IsNullOrEmpty(section.GetStringValue(newIndex.ToString(), string.Empty)))
+ newIndex++;
+
+ section.SetStringValue(newIndex.ToString(), value);
+ ListAITargetTypes();
+ editedIndex = newIndex;
+ SelectCurrentEntry();
+ }
+
+ private void BtnAddTechno_LeftClick(object sender, EventArgs e)
+ {
+ if (editedIndex == -1 || lbAvailableTechnos.SelectedItem == null)
+ return;
+
+ var techno = (GameObjectType)lbAvailableTechnos.SelectedItem.Tag;
+ lbTechnoEntries.AddItem(new XNAListBoxItem { Text = techno.ININame, Tag = techno });
+ CommitTechnoEntries();
+ }
+
+ private void BtnDeleteTechno_LeftClick(object sender, EventArgs e)
+ {
+ if (editedIndex == -1 || lbTechnoEntries.SelectedItem == null)
+ return;
+
+ int selected = lbTechnoEntries.SelectedIndex;
+ var remaining = lbTechnoEntries.Items.Where((item, index) => index != selected).Select(item => item.Text).ToList();
+
+ lbTechnoEntries.Clear();
+ foreach (var entry in remaining)
+ lbTechnoEntries.AddItem(new XNAListBoxItem { Text = entry, Tag = entry });
+
+ CommitTechnoEntries();
+ lbTechnoEntries.SelectedIndex = Math.Max(0, selected - 1);
+ }
+
+ private void CommitTechnoEntries()
+ {
+ if (editedIndex == -1)
+ return;
+
+ string combined = string.Join(",", lbTechnoEntries.Items.Select(item => item.Text).Where(text => !string.IsNullOrWhiteSpace(text)));
+
+ var section = map.LoadedINI.GetSection("AITargetTypes");
+ if (section == null)
+ {
+ map.LoadedINI.AddSection("AITargetTypes");
+ section = map.LoadedINI.GetSection("AITargetTypes");
+ }
+
+ section.SetStringValue(editedIndex.ToString(), combined);
+ ListAITargetTypes();
+ SelectCurrentEntry();
+ }
+
+ private void SelectCurrentEntry()
+ {
+ for (int i = 0; i < lbAITargetTypes.Items.Count; i++)
+ {
+ if ((int)lbAITargetTypes.Items[i].Tag == editedIndex)
+ {
+ lbAITargetTypes.SelectedIndex = i;
+ return;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TSMapEditor/UI/Windows/WindowController.cs b/src/TSMapEditor/UI/Windows/WindowController.cs
index 87babec6..b7af373a 100644
--- a/src/TSMapEditor/UI/Windows/WindowController.cs
+++ b/src/TSMapEditor/UI/Windows/WindowController.cs
@@ -42,6 +42,7 @@ public class WindowController
public TeamTypesWindow TeamTypesWindow { get; private set; }
public TriggersWindow TriggersWindow { get; private set; }
public AITriggersWindow AITriggersWindow { get; private set; }
+ public AITargetTypesWindow AITargetTypesWindow { get; private set; }
public PlaceWaypointWindow PlaceWaypointWindow { get; private set; }
public LocalVariablesWindow LocalVariablesWindow { get; private set; }
public StructureOptionsWindow StructureOptionsWindow { get; private set; }
@@ -125,6 +126,9 @@ public void Initialize(IWindowParentControl windowParentControl, Map map, Editor
AITriggersWindow = new AITriggersWindow(windowParentControl.WindowManager, map);
Windows.Add(AITriggersWindow);
+ AITargetTypesWindow = new AITargetTypesWindow(windowParentControl.WindowManager, map);
+ Windows.Add(AITargetTypesWindow);
+
PlaceWaypointWindow = new PlaceWaypointWindow(windowParentControl.WindowManager, map, cursorActionTarget.MutationManager, cursorActionTarget.MutationTarget);
Windows.Add(PlaceWaypointWindow);