Skip to content

Commit 13b8a69

Browse files
authored
Addessed same-named animations/categories by not allowing it in Gum. (#2554)
1 parent 13c3bfe commit 13b8a69

14 files changed

Lines changed: 187 additions & 8 deletions

File tree

GEMINI.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,31 @@ Before proceeding with any request, you **MUST** read and adhere to the guidelin
1111
1. **Project Overview & Build/Test Rules**: Read **`CLAUDE.md`**. Use the solution-based building and testing workflows defined there.
1212
2. **Code Style & Architecture**: Read **`.claude/code-style.md`**.
1313
3. **Task-Specific Logic (Skills)**: Search the **`.claude/skills/`** directory. If a skill exists for the current task (e.g., `gum-cross-platform-unification`), you MUST load its `SKILL.md` and follow its specific checklists and status tracking.
14+
15+
## Agent Workflow
16+
For every task, invoke the appropriate agent from `.claude/agents/` before proceeding. The agent's instructions provide guidelines for how the task should be performed. Before doing any work, announce which agent you are using such as "Invoking coder agent for this task..."
17+
18+
Available agents:
19+
- **coder** — Writing or modifying code and unit tests for new features or bugs
20+
- **qa** — Testing, reviewing changes, and verifying correctness
21+
- **refactoring-specialist** — Refactoring and improving code structure
22+
- **docs-writer** — Writing or updating documentation
23+
- **product-manager** — Breaking down tasks and tracking progress
24+
- **security-auditor** — Security reviews and vulnerability assessments
25+
26+
Select the agent that best matches the task at hand. For tasks that span multiple concerns (e.g., implement a feature and write tests), invoke the relevant agents in sequence.
27+
28+
## Building and Testing
29+
30+
**Always build and test via a solution file**, not individual `.csproj` files. Plugin projects use `$(SolutionDir)` in post-build scripts, which is undefined when building a `.csproj` directly.
31+
32+
Pick the solution based on what you're working on:
33+
34+
* **`GumFull.sln`** — the Gum tool and all tool-related projects (WPF editor, plugins, `GumToolUnitTests`, `Gum.Cli.Tests`, etc.). Use this when working on anything under `Tool/`, `Gum/`, plugins, or tool-side code.
35+
* **`AllLibraries.sln`** — all runtime-related projects (`GumCommon`, `MonoGameGum`, `KniGum`, `FnaGum`, `SkiaGum`, `RaylibGum`, and their test projects including `MonoGameGum.Tests`). Use this when working on runtime libraries, `GumCommon`, or anything a shipped game would reference.
36+
37+
Examples:
38+
* Build: `dotnet build GumFull.sln` or `dotnet build AllLibraries.sln`
39+
* Test: `dotnet test AllLibraries.sln --filter "TestClassName"`
40+
41+
If a change spans both (e.g. editing `GumCommon` which is linked into both), build both solutions.

Gum/Managers/INameVerifier.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public interface INameVerifier
2020

2121
bool IsInstanceNameValid(string instanceName, InstanceSave instanceSave, IInstanceContainer instanceContainer, out string? whyNotValid);
2222

23+
bool IsNameValidTopLevel(string name, ElementSave element, object? objectToIgnore, out string? whyNotValid);
24+
2325
bool IsVariableNameValid(string variableName, ElementSave elementSave, VariableSave variableSave, out string? whyNotValid);
2426

2527
bool IsBehaviorNameValid(string behaviorName, BehaviorSave behaviorSave, out string? whyNotValid);

Gum/Managers/NameVerifier.cs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Gum.DataTypes.Variables;
44
using Gum.Logic;
55
using System.CodeDom.Compiler;
6+
using System.Collections.Generic;
67
using System.Collections.Immutable;
78
using System.Globalization;
89
using System.Linq;
@@ -137,15 +138,17 @@ public class NameVerifier : INameVerifier
137138

138139
private readonly StandardElementsManager _standardElementsManager;
139140
private readonly IVariableSaveLogic _variableSaveLogic;
141+
private readonly Plugins.IPluginManager _pluginManager;
140142

141143
#endregion
142144

143145
#region Folder
144146

145-
public NameVerifier(IVariableSaveLogic variableSaveLogic)
147+
public NameVerifier(IVariableSaveLogic variableSaveLogic, Plugins.IPluginManager pluginManager)
146148
{
147149
_standardElementsManager = StandardElementsManager.Self;
148150
_variableSaveLogic = variableSaveLogic;
151+
_pluginManager = pluginManager;
149152
}
150153
public bool IsFolderNameValid(string? folderName, out string whyNotValid)
151154
{
@@ -208,6 +211,11 @@ public bool IsCategoryNameValid(string? name, IStateContainer categoryContainer,
208211
}
209212
}
210213

214+
if(string.IsNullOrEmpty(whyNotValid) && categoryContainer is ElementSave element)
215+
{
216+
IsNameValidTopLevel(name, element, null, out whyNotValid);
217+
}
218+
211219
return string.IsNullOrEmpty(whyNotValid);
212220
}
213221
public bool IsStateNameValid(string name, StateSaveCategory category, StateSave stateSave, out string whyNotValid)
@@ -247,6 +255,12 @@ public bool IsInstanceNameValid(string instanceName, InstanceSave instanceSave,
247255
{
248256
IsNameAlreadyUsed(instanceName, instanceSave, instanceContainer, out whyNotValid);
249257
}
258+
259+
if (string.IsNullOrEmpty(whyNotValid) && instanceContainer is ElementSave element)
260+
{
261+
IsNameValidTopLevel(instanceName, element, instanceSave, out whyNotValid);
262+
}
263+
250264
return string.IsNullOrEmpty(whyNotValid);
251265
}
252266
private void IsNameUsedByStandardVariables(string nameToCheck, out string whyNotValid)
@@ -263,6 +277,43 @@ private void IsNameUsedByStandardVariables(string nameToCheck, out string whyNot
263277
whyNotValid = $"\"Name\" is a reserved keyword so it cannot be used as a name";
264278
}
265279
}
280+
public bool IsNameValidTopLevel(string name, ElementSave element, object? objectToIgnore, out string? whyNotValid)
281+
{
282+
whyNotValid = null;
283+
var names = new List<TopLevelName>();
284+
285+
foreach (var instance in element.Instances)
286+
{
287+
names.Add(new TopLevelName(instance.Name, "Instance", instance));
288+
}
289+
290+
foreach (var category in element.Categories)
291+
{
292+
names.Add(new TopLevelName(category.Name, "Category", category));
293+
}
294+
295+
if (element.DefaultState != null)
296+
{
297+
foreach (var variable in element.DefaultState.Variables.Where(item => !string.IsNullOrEmpty(item.ExposedAsName)))
298+
{
299+
names.Add(new TopLevelName(variable.ExposedAsName, "Variable", variable));
300+
}
301+
}
302+
303+
_pluginManager.FillTopLevelNames(element, names);
304+
305+
string standardizedName = Standardize(name);
306+
307+
var match = names.FirstOrDefault(item => item.SourceObject != objectToIgnore && Standardize(item.Name) == standardizedName);
308+
309+
if (match != null)
310+
{
311+
whyNotValid = $"The name {name} is already used by a(n) {match.Type}";
312+
return false;
313+
}
314+
315+
return true;
316+
}
266317
public bool IsVariableNameValid(string variableName, ElementSave elementSave, VariableSave variableSave, out string whyNotValid)
267318
{
268319
whyNotValid = null;
@@ -280,6 +331,12 @@ public bool IsVariableNameValid(string variableName, ElementSave elementSave, Va
280331
{
281332
IsNameAlreadyUsed(variableName, variableSave, elementSave, out whyNotValid);
282333
}
334+
335+
if (string.IsNullOrEmpty(whyNotValid) && elementSave != null)
336+
{
337+
IsNameValidTopLevel(variableName, elementSave, variableSave, out whyNotValid);
338+
}
339+
283340
if (string.IsNullOrEmpty(whyNotValid))
284341
{
285342
var existingVariable = elementSave?.GetVariableFromThisOrBase(variableName);

Gum/Managers/TopLevelName.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Gum.Managers;
2+
3+
public record TopLevelName(string Name, string Type, object? SourceObject);

Gum/Plugins/BaseClasses/PluginBase.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ protected PluginBase()
260260
_dialogService = Locator.GetRequiredService<IDialogService>();
261261
}
262262

263+
public virtual void FillTopLevelNames(ElementSave element, List<TopLevelName> names) { }
264+
263265
public abstract void StartUp();
264266
public abstract bool ShutDown(PluginShutDownReason shutDownReason);
265267

Gum/Plugins/IPluginManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public interface IPluginManager
140140
IEnumerable<IPositionedSizedObject>? GetSelectedIpsos();
141141
System.Numerics.Vector2? GetWorldCursorPosition(InputLibrary.Cursor cursor);
142142
void FillWithErrors(List<ErrorViewModel> errors, PluginBase? plugin = null);
143+
void FillTopLevelNames(ElementSave element, List<TopLevelName> names);
143144
bool GetIfShouldSuppressRemoveEditorHighlight();
144145
void FocusSearch();
145146
bool ShouldExclude(VariableSave defaultVariable, RecursiveVariableFinder rvf);

Gum/Plugins/PluginManager.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,9 @@ public void FillWithErrors(List<ErrorViewModel> errors, PluginBase? plugin = nul
657657

658658
}
659659

660+
public void FillTopLevelNames(ElementSave element, List<TopLevelName> names) =>
661+
CallMethodOnPlugin(plugin => plugin.FillTopLevelNames(element, names));
662+
660663
/// <summary>
661664
/// Returns whether any plugins are asking un-highlighting to be suppressed when moving over the editor.
662665
/// If true, then the editor will not un-highlight the currently-highlighted object. This allows other plugins

Gum/StateAnimationPlugin/MainStateAnimationPlugin.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ public override Version Version
6868
get { return new Version(0, 0, 0, 2); }
6969
}
7070

71+
public override void FillTopLevelNames(ElementSave element, List<TopLevelName> names)
72+
{
73+
var animationsSave = _animationCollectionViewModelManager.GetElementAnimationsSave(element);
74+
if (animationsSave != null)
75+
{
76+
foreach (var anim in animationsSave.Animations)
77+
{
78+
names.Add(new TopLevelName(anim.Name, "Animation", anim));
79+
}
80+
}
81+
}
82+
7183
ObservableCollection<string> AvailableStates { get; set; } = new ObservableCollection<string>();
7284

7385
#endregion

Gum/StateAnimationPlugin/Validation/NameValidator.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public NameValidator(INameVerifier nameVerifier)
1616
{
1717
_nameVerifier = nameVerifier;
1818
}
19-
public bool IsAnimationNameValid(string? animationName,
20-
IEnumerable<AnimationViewModel> existingAnimations, out string? whyNotValid)
19+
public bool IsAnimationNameValid(string? animationName, Gum.DataTypes.ElementSave element,
20+
IEnumerable<AnimationViewModel> existingAnimations, out string? whyNotValid, object? objectToIgnore = null)
2121
{
2222
whyNotValid = null;
2323

@@ -35,11 +35,16 @@ public bool IsAnimationNameValid(string? animationName,
3535
{
3636
return false;
3737
}
38-
if(existingAnimations.Any(item=>item.Name.Equals(animationName, StringComparison.InvariantCultureIgnoreCase)))
38+
if(existingAnimations.Any(item=>item.Name.Equals(animationName, StringComparison.InvariantCultureIgnoreCase) && item != objectToIgnore))
3939
{
4040
whyNotValid = $"The name \"{animationName}\" is already being used.";
4141
return false;
4242
}
43+
44+
if(element != null)
45+
{
46+
_nameVerifier.IsNameValidTopLevel(animationName, element, objectToIgnore, out whyNotValid);
47+
}
4348

4449
return string.IsNullOrEmpty(whyNotValid);
4550
}

Gum/StateAnimationPlugin/ViewModels/AddAnimationDialogViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ public string Name
1414
{
1515
if (Set(value))
1616
{
17+
NotifyPropertyChanged(nameof(ValidationMessage));
1718
AffirmativeCommand.NotifyCanExecuteChanged();
1819
}
1920
}
2021
}
2122

23+
public string? ValidationMessage => Validator?.Invoke(Name);
24+
2225
public bool Loops
2326
{
2427
get => Get<bool>();

0 commit comments

Comments
 (0)