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);