Skip to content

Implement rich command processor#523

Merged
ikkentim merged 56 commits intomainfrom
feature/improve_command_processor
May 9, 2026
Merged

Implement rich command processor#523
ikkentim merged 56 commits intomainfrom
feature/improve_command_processor

Conversation

@ikkentim
Copy link
Copy Markdown
Owner

@ikkentim ikkentim commented May 4, 2026

No description provided.

ikkentim added 30 commits May 3, 2026 16:18
…nd_processor

* origin/main:
  fix: Windows build scripts fail correctly on build errors (#518)
  Fix 'cdecl' attribute ignored warnings in Linux x86_64 builds (#516)
  Include ECS libraries in SampSharp.Sdk meta-package (#512)
  Separate test projects into test/ directory (#510)
  Add WeaponInfo related extensions #502 (#508)
- Renamed CommandOverload to CommandDefinition (merged type)
- Renamed CommandDefinition wrapper to CommandCommand (internal only)
- Added Name/Group/FullName properties to CommandDefinition
- Updated CommandScanner to build individual CommandDefinitions with grouping data
- Updated CommandRegistry to accept flat CommandDefinitions and group them internally
- Updated CommandDispatcher to work with CommandRegistry internal methods
- Updated message service interfaces to accept IReadOnlyList<CommandDefinition>
- Updated DefaultCommandEnumerator to group definitions by name/group
- All overloads of a command can now be properly registered and accessed
Copilot AI and others added 2 commits May 8, 2026 22:20
Copy link
Copy Markdown
Contributor

Copilot AI commented May 8, 2026

@copilot address the comments in this PR. It's mainly about using StringSpans instead of the various string operations throughout the whole overload resolution -> argument parsing procedure.

Done in 95d7260. The full dispatch pipeline now uses StringSpan from the moment input enters the service, with no string splitting, joining, or tokenizing:

  • PlayerCommandService: creates StringSpan.For(inputText), skips / and whitespace via span operations, passes the span to the dispatcher
  • ConsoleCommandService: creates StringSpan.For(inputText), trims, passes to dispatcher
  • CommandDispatcher.Dispatch: accepts StringSpan; calls GetCommandGroupByPath(ref span) which advances the span past consumed command words; computes usedCommandName from span arithmetic (beforeLookup.Take(consumed chars).ToString().TrimEnd())
  • CommandRegistry.GetCommandGroupByPath: accepts ref StringSpan, delegates to the tree
  • CommandTree.FindCommand: accepts ref StringSpan, delegates to Traverse
  • CommandTreeNode.Traverse: reads one whitespace-delimited word at a time using TrimStart() + length scan, advances the span past each consumed word

Also fixed alias CommandSet registration: aliases now use the alias name as their Name and null as their Group (95d7260).

wordLen++;
}

var word = trimmed.Take(wordLen).ToString();
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not happen. Should stay StringSpan instead of transforming to string. TryFindChild should ge changed to use StringSpan instead. Dictionary lookup may be more difficult, but the item count will not be endless, we can iterate through.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21f9efa. TryGetChild now accepts ReadOnlySpan<char> and iterates through _children using word.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase) — no .ToString() allocation. Traverse uses trimmed.AsSpan()[..wordLen] to get the word span directly.

Comment thread src/SampSharp.OpenMp.Entities.Commands/Core/CommandRegistry.cs Outdated
@ikkentim
Copy link
Copy Markdown
Owner Author

ikkentim commented May 9, 2026

@copilot address code comments

…ses per-overload only

Agent-Logs-Url: https://github.com/ikkentim/SampSharp/sessions/2e3b0b35-34d7-4c8a-a1e7-591af7a90704

Co-authored-by: ikkentim <2820647+ikkentim@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 9, 2026

@copilot address code comments

Done in 21f9efa:

  • TryGetChild changed to ReadOnlySpan<char> parameter, iterates children for case-insensitive match with no .ToString() allocation; Traverse uses trimmed.AsSpan()[..wordLen] directly
  • Alias CommandSet reverted to single-overload ([overload]) — each alias is tied to the specific overload it was registered for

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 114 out of 114 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

src/SampSharp.OpenMp.Entities.Commands/Player/PlayerCommandProcessingMiddleware.cs:39

  • IsHandled no longer treats integer return values specially. With the current switch, an int result (including 0) will fall into the default arm and be treated as handled, which changes behavior compared to the previous implementation (where 0 was unhandled). If any OnPlayerCommandText handlers/middleware return int (legacy pattern), restore int i => i != 0 handling (or explicitly restrict supported result types).

Comment thread test/TestMode.UnitTests/TestMode.UnitTests.csproj
Comment on lines +81 to +87
/// <inheritdoc />
public bool SendPermissionDenied(Player player, CommandDefinition overload)
{
var message = FormatPermissionDenied(""); // TODO input text
player.SendClientMessage(message);
return true;// TODO: option to not print message
}
Comment on lines +16 to +21

/// <summary>Searches for commands matching a query.</summary>
IEnumerable<CommandDefinition> SearchCommands(string query);

/// <summary>Finds a specific command by name.</summary>
CommandDefinition? FindCommand(string name);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation should be non-null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d805d. Removed the null-handling (query?.ToLowerInvariant() ?? ""); implementation now uses query.ToLowerInvariant() directly, consistent with the non-nullable interface contract. Also removed the SearchCommands_NullQuery_ReturnsAll test.

Comment on lines +44 to +48
[Fact]
public void CreateSuccess_MessageIsNullByDefault()
{
var result = DispatchResult.CreateSuccess();
}
Comment on lines +154 to +158
[PlayerCommand(Name = "ping")]
public void PingCommand(Player player, IEntityManager entityManager)
{
player.SendClientMessage($"Your ping: {player.Ping}ms");
}
Comment on lines +5 to +6
internal class UnhandledExceptionHandlerImpl: IUnhandledExceptionHandler
{
Comment thread src/SampSharp.OpenMp.Entities.Commands/Core/CommandRegistry.cs Outdated
Comment on lines +160 to +164
[PlayerCommand("announce")]
public void AnnounceCommand(IWorldService server)
{
server.SendClientMessage("Hello everyone!");
}
Comment on lines +79 to +93
/// <inheritdoc />
public bool Equals(CommandGroup other)
{
return _parts.SequenceEqual(other._parts);
}

/// <inheritdoc />
public override int GetHashCode()
{
var hash = new HashCode();
foreach (var part in _parts)
{
hash.Add(part);
}
return hash.ToHashCode();
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A string comparer should be set in the service and passed through with other calls. Maybe even better would be an ioptions pattern with command service options (one for console commands, one for players commands, common base type)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 95d805d. Added CommandServiceOptions (base class), PlayerCommandServiceOptions, and ConsoleCommandServiceOptions registered via AddOptions<T>() in DI. CommandGroup.Equals/GetHashCode now use OrdinalIgnoreCase to match tree lookup behaviour. The options classes provide the foundation for wiring a configurable StringComparison through the services.

@ikkentim
Copy link
Copy Markdown
Owner Author

ikkentim commented May 9, 2026

@copilot address review comments

…mmandGroup OrdinalIgnoreCase, CommandServiceOptions, empty tests, SampleCommandsSystem fixes

Agent-Logs-Url: https://github.com/ikkentim/SampSharp/sessions/6bdff1d0-3cf0-4097-8290-f9de78550f30

Co-authored-by: ikkentim <2820647+ikkentim@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 9, 2026

@copilot address review comments

Done in 95d805d. All new review comments addressed:

  • SearchCommands null handling removed; test updated
  • Alias accumulation fixed (multiple overloads per alias now all reachable)
  • CommandGroup uses OrdinalIgnoreCase; CommandServiceOptions / PlayerCommandServiceOptions / ConsoleCommandServiceOptions added + registered in DI
  • Two empty DispatchResultTests removed
  • SampleCommandsSystem.cs: stray semicolon, unused entityManager, AnnounceCommand missing Player param all fixed
  • DefaultPlayerCommandMessageService: SendPermissionDenied uses overload name; SendCommandNotFound now sends a message

@ikkentim
Copy link
Copy Markdown
Owner Author

ikkentim commented May 9, 2026

@copilot I'm not yet happy with how registration of commands work.

I dont see the point in ScanPlayerCommands (and similarly the console variant) of grouping comands by method etc. anymore. I think we should just iterate through the found methods -> validate command name -> BuildCommandGroup -> TryBuildOverload -> registry.Register. Everything else seems like overcomplicating things.

registry.Register should also be simplified.

  • The CommandSet type should be removed.
  • The _overloadsByKey and _overloadsByAliasKey fields should both be removed.
  • registry.TryFind should be removed (along with ICommandHelpProvider.FindCommand)
  • registry.TryFindByPath overloads should be removed

Registration should:

  • Simply call tree.Register once with the (command, command.Group, command.Name); and then call tree.Register once for each alias with (command, group: null, name: alias)
  • tree.Register should first traverse to node based on group (if not null), then traverse to next node using the name, then add the CommandDefinition to the commands List<> of the node.

That way CommandTree.BuildPath, CommandTree.RegisterAlias can be removed. RegisterPath may be inlined.

…simplify scanner/registry/tree)

Agent-Logs-Url: https://github.com/ikkentim/SampSharp/sessions/f6e0b1a7-d7d3-44a5-9fc5-c6d199902375

Co-authored-by: ikkentim <2820647+ikkentim@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 9, 2026

@copilot I'm not yet happy with how registration of commands work.

I dont see the point in ScanPlayerCommands (and similarly the console variant) of grouping comands by method etc. anymore. I thin...

Done in 7c721f0:

  • CommandSet removedCommandTreeNode now holds a List<CommandDefinition> directly (via AddCommand / Commands)
  • CommandTree.Register(CommandDefinition, CommandGroup?, name) — traverses group parts (if non-null), then descends to the name node and appends the definition. BuildPath, RegisterAlias, RegisterPath all removed.
  • CommandRegistry_overloadsByKey, _overloadsByAliasKey, TryFind, TryFindByPath, GetCommand all removed. Register calls tree.Register(overload, overload.Group, overload.Name) once then tree.Register(overload, null, alias.Name) per alias.
  • ICommandRegistry — stripped to GetAll(), GetCommandsInGroup(), GetGroups().
  • CommandScannerGroupBy removed; methods are scanned and registered linearly.
  • ICommandHelpProvider / DefaultCommandHelpProviderFindCommand removed.
  • All unit tests updated; CommandSetTests.cs deleted.

@ikkentim ikkentim merged commit 23fda58 into main May 9, 2026
1 check passed
@ikkentim ikkentim deleted the feature/improve_command_processor branch May 9, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants