diff --git a/AGENTS.md b/AGENTS.md index 13731e8..3e9aa3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,8 @@ This file summarizes all essential knowledge for writing correct cross-reference ``` ``` -- Or, for Markdown link text: + This is the **preferred** syntax. +- Alternatively, for custom Markdown link text: ``` [Link Text](xref:my-unique-id) ``` @@ -33,7 +34,8 @@ This file summarizes all essential knowledge for writing correct cross-reference - Always check that the target file has a `uid` in its YAML frontmatter. - Use short, descriptive UIDs (lowercase, hyphens/underscores if needed). - Never duplicate UIDs across files. -- Prefer `xref:` links over relative links for conceptual docs when possible. +- **Prefer `` syntax** over `[text](xref:uid)` for clarity and simplicity. +- Use markdown link text syntax only when you need custom link text that differs from the page title. - For API docs, use the full type/member name as UID (e.g., `System.String`, `MyNamespace.MyClass.MyMethod`). ## 4. Troubleshooting @@ -45,7 +47,7 @@ This file summarizes all essential knowledge for writing correct cross-reference ## 5. Example ```markdown -See [the events article](xref:events) for more details. +See for more details. ``` ## 6. Useful Links diff --git a/docs/core-concepts/index.md b/docs/core-concepts/index.md index e11ad6c..8f1513e 100644 --- a/docs/core-concepts/index.md +++ b/docs/core-concepts/index.md @@ -87,14 +87,4 @@ Here's how ECS operates in practice with SampSharp: 2. open.mp fires a vehicle exit event 3. A system receives the event with the `Player` and `Vehicle` components 4. The system calculates a fare based on distance traveled and gives money to the player -5. The system logs the ride for statistics - -### Benefits of ECS - -The ECS pattern offers several advantages for SampSharp game mode development: - -- **Flexibility** - SampSharp's domain-specific components map directly to SA-MP/open.mp concepts, making development intuitive -- **Decoupling** - Game logic (systems) is separate from data (components), making code easier to test and modify -- **Maintainability** - Each component handles a distinct aspect of a game entity, keeping concerns separated -- **Composability** - Complex entities are built by combining simpler components -- **Extensibility** - Add new systems or custom components without modifying existing code +5. The system logs the ride for statistics \ No newline at end of file diff --git a/docs/features/commands.md b/docs/features/commands.md index e7ac522..974d693 100644 --- a/docs/features/commands.md +++ b/docs/features/commands.md @@ -5,18 +5,442 @@ uid: commands # Implementing Commands -> [!NOTE] -> This article is coming soon! Check back later, or feel free to open an issue if you have questions. - - +The SampSharp command system provides a declarative way to handle player and console commands through attributes. Commands are discovered automatically from implementations and can include complex features like overloading, aliasing, command groups, and permission checking. + +## Setup + +To enable the command system, call `UseCommands()` in your .Initialize() method: + +```csharp +public class Startup : IEcsStartup +{ + public void Initialize(IStartupContext context) + { + context.UseEntities() + .UseCommands(); + } +} +``` + +## Core Concepts + +### Player Commands vs Console Commands + +**Player Commands** are invoked when a player types a message starting with `/` in the game chat: +- Always require a parameter as the first argument +- Automatically receive the player who executed the command +- Intended for in-game gameplay commands + +**Console Commands** (also known as **RCON commands**) are executed from the server console: +- Execute without automatic player context (typically server-side) +- Optional parameter can provide player context if the command was executed by a player +- Do not have automatic permission checking +- Useful for server administration and debugging + +### Registering Command Handlers + +Command methods must be part of an implementation. The command system automatically discovers and registers these methods: + +```csharp +public class MyCommandsSystem(IEntityManager entityManager) : ISystem +{ + [PlayerCommand(Name = "hello")] + public void HelloCommand(Player player) + { + player.SendClientMessage("Hello!"); + } + + [ConsoleCommand(Name = "server_status")] + public void ServerStatus() + { + Console.WriteLine("Server is running."); + } +} +``` + +### Command Naming + +Command names come from the `Name` property of the attribute. If `Name` is not specified, the command system infers it from the method name: +- Method name is converted to lowercase +- If the method name ends with `"Command"`, that suffix is removed + +**Examples:** +- Method `KillCommand()` → command name `kill` +- Method `ServerStatus()` → command name `server_status` +- Method `StatusCommand()` → command name `status` +- Method `Help()` → command name `help` + +By default, command names are **case-insensitive**, so `/Kill`, `/KILL`, and `/kill` all invoke the same command. If you need exact case matching, configure case sensitivity in your startup: + +```csharp +public class Startup : IEcsStartup +{ + public void Initialize(IStartupContext context) + { + context.UseEntities() + .UsePlayerCommands(cfg => cfg.StringComparison = StringComparison.Ordinal) + .UseConsoleCommands(cfg => cfg.StringComparison = StringComparison.Ordinal); + } +} +``` + +With `StringComparison.Ordinal`, `/kill` and `/Kill` are now treated as different commands. + +## Player Commands + +### PlayerCommandAttribute + +The `[PlayerCommand]` attribute marks a method as a player command: + +```csharp +[PlayerCommand(Name = "kill")] +public void KillCommand(Player player) +{ + player.Health = 0; +} +``` + +**Capabilities:** +- Automatically detects the first parameter type to determine who can execute the command +- Supports or custom types as the first parameter + +**Parameter Resolution:** +The first parameter determines command execution context: +- If : the command can be executed by any player +- If custom (like a `VIPPlayer` component): the command only works for players with that component +- Subsequent parameters are parsed from user input + +**Example:** + +```csharp +[PlayerCommand(Name = "spawn")] +public void SpawnCommand(Player player, VehicleModelType model, IWorldService worldService) +{ + player.SendClientMessage($"Spawned a {model}!"); + var vehicle = worldService.CreateVehicle(model, player.Position, player.Angle, -1, -1); + player.PutInVehicle(vehicle); +} +``` + +## Console Commands + +### ConsoleCommandAttribute + +The `[ConsoleCommand]` attribute marks a method as a console command: + +**Capabilities:** +- Optional parameter (if present, must be the first parameter) for command metadata +- No permission checking (intended for server-side use) + +See the Registering Command Handlers section above for a console command example. + +## Options + +### Command Aliasing + +Use `[Alias]` to provide shorthand names for commands. Multiple aliases are supported: + +```csharp +[CommandGroup("economy")] +[PlayerCommand(Name = "money")] +[Alias("$", "cash")] +public void MoneyCommand(Player player) +{ + player.SendClientMessage($"Money: ${player.Money}"); +} +``` + +Now `/economy money`, `/$`, and `/cash` all work. Note that aliases bypass the command group—they're global shortcuts regardless of the group hierarchy. Aliases work for both player and console commands. + +### Command Groups + +Command groups organize commands into hierarchies. Apply groups to the entire system class or to individual methods using `[CommandGroup]`: + +```csharp +[CommandGroup("admin")] +public class AdminCommandsSystem : ISystem +{ + [PlayerCommand(Name = "kick")] + public void KickCommand(Player player, Player target) { } + + [PlayerCommand(Name = "ban")] + public void BanCommand(Player player, Player target) { } +} +``` + +This creates `/admin kick` and `/admin ban` commands. Groups can be stacked for deeper hierarchies by applying multiple `[CommandGroup]` attributes. + +### Command Overloading + +Multiple command handlers can have the same name with different parameter signatures. This is useful for commands that support different use cases: + +```csharp +[CommandGroup("teleport")] +[PlayerCommand(Name = "player")] +public void TeleportCommand(Player player, Player target) +{ + // /teleport player + player.Position = target.Position; + player.SendClientMessage($"Teleported to {target.Name}"); +} + +[CommandGroup("teleport")] +[PlayerCommand(Name = "player")] +public void TeleportCommand(Player player, float x, float y, float z) +{ + // /teleport player + player.Position = new Vector3(x, y, z); + player.SendClientMessage($"Teleported to ({x}, {y}, {z})"); +} + +[CommandGroup("teleport")] +[PlayerCommand(Name = "player")] +[Alias("tp")] +public void TeleportCommand(Player player, Player target, float x, float y, float z) +{ + // /teleport player OR /tp + // Admin command to teleport another player + target.Position = new Vector3(x, y, z); + player.SendClientMessage($"Teleported {target.Name} to ({x}, {y}, {z})"); +} +``` + +The command system automatically selects the correct overload based on parameter types and count: +- `/teleport player Johnny` calls the first overload (Player parameter) +- `/teleport player 100 200 50` calls the second overload (three float parameters) +- `/teleport player Johnny 100 200 50` or `/tp Johnny 100 200 50` calls the third overload (Player + three floats) + +Command overloading works for both player and console commands. + +### Optional Parameters + +Commands support optional parameters to make command syntax more flexible. Optional parameters can have default values: + +```csharp +[PlayerCommand(Name = "money")] +public void MoneyCommand(Player player, int? amount = null) +{ + if (amount.HasValue) + { + player.Money = amount.Value; + player.SendClientMessage($"Money set to ${amount.Value}"); + } + else + { + player.SendClientMessage($"Current money: ${player.Money}"); + } +} +``` + +Now `/money` displays the current money, while `/money 5000` sets it to 5000. + +### Dependency Injection + +Commands can receive dependencies from your service collection. Any registered service can be injected as a parameter. Dependency injection parameters can appear anywhere in the method signature, even before optional parameters: + +```csharp +[PlayerCommand(Name = "money")] +public void MoneyCommand(Player player, IWorldService worldService, int? amount = null) +{ + if (amount.HasValue) + { + player.Money = amount.Value; + player.SendClientMessage($"Money set to ${amount.Value}"); + } + else + { + player.SendClientMessage($"Current money: ${player.Money}"); + } +} +``` + +The injected `IWorldService` is provided automatically, while `amount` remains optional from user input. + +### Command Introspection + +The provides access to all registered commands: + +```csharp +[PlayerCommand(Name = "help")] +public void HelpCommand(Player player, IPlayerCommandService commands) +{ + player.SendClientMessage("--- Available Commands ---"); + + var commandList = commands.Registry.GetAll() + .OrderBy(c => c.Name) + .ToList(); + + foreach (var cmd in commandList) + { + var aliases = cmd.Aliases.Count > 0 + ? $" ({string.Join(", ", cmd.Aliases.Select(a => $"/{a.Name}"))})" + : ""; + player.SendClientMessage($"/{cmd.Name}{aliases}"); + } +} +``` + +### Player Component Checking + +Instead of (or `EntityId`), a custom can be used as the first parameter of a player command. In this case, the command is only available to players who have that component attached. + +```csharp +public class CivilianComponent : Component { } + +[PlayerCommand(Name = "civonly")] +public void CivOnlyCommand(CivilianComponent civilian) +{ + // Only players with the CivilianComponent can use this command + civilian.Player.SendClientMessage("You are a civilian!"); +} +``` + +This allows for role-based or feature-based command access control using your own types. + +### Command Tags + +Use `[CommandTag]` to attach custom metadata to commands as key-value pairs. Tags can be used for permission checking, categorization, or any other custom logic. + +```csharp +[PlayerCommand(Name = "kick")] +[CommandTag("permission", "admin")] +[CommandTag("category", "moderation")] +public void KickCommand(Player player, Player target) +{ + target.Kick(); +} +``` + +Multiple tags can be applied to the same command. Tags are typically used by custom implementations to enforce access control: + +```csharp +public class MyPermissionChecker : IPermissionChecker +{ + public bool HasPermission(Player player, CommandDefinition command) + { + var permission = command.GetTag("permission"); + if (permission == null) + return true; // No permission requirement + + return player.HasPermission(permission); + } +} +``` + +## Return Values + +Command methods can return values to indicate success or failure: + +- `void` - Standard synchronous command execution (always treated as success) +- `bool` - Returns `true` to indicate success, or `false` to indicate the command was not recognized (proceeds as if command was not found) +- `Task` - Asynchronous command execution (always treated as success) +- `Task` - For synchronous completion, the bool indicates success/failure. For async completion, the task completion is assumed to be success regardless of the return value + +## Parameter Types + +The command system automatically parses the following parameter types from user input: + +- `int`, `float`, `double` - Numeric types +- `bool` - Boolean values (`true`/`false`) +- `string` - Text input (greedily consumes remaining input for the last parameter, or a single word for non-last parameters) +- `Player` or `EntityId` - Player matched by player ID or player name +- Enum types - Parsed by name (case-insensitive) + +Any other parameter type not in this list is treated as a dependency injection parameter and resolved from the service collection. + +## Customization + +The command system provides extension points for customization: permission checking, text formatting, and custom parameter type parsing. + +### Permission Checking + +Implement to define custom permission logic for player commands. The default implementation allows all commands. + +```csharp +public class MyPermissionChecker : IPermissionChecker +{ + public bool HasPermission(Player player, CommandDefinition command) + { + // Check if the command has a "permission" tag + var permission = command.GetTag("permission"); + if (permission == null) + return true; // No permission requirement + + // Check your permission system + return player.IsAdmin || HasPlayerPermission(player, permission); + } + + private bool HasPlayerPermission(Player player, string permission) + { + // Implement your game's permission system here + return false; + } +} +``` + +Register your custom permission checker in your startup class to replace the default: + +```csharp +services.RemoveAll(typeof(IPermissionChecker)); +services.AddSingleton(); +``` + +### Custom Text Formatting + +Implement to customize how command usage, not found, and other error messages are formatted. This is useful for localization or custom message styles. + +```csharp +public class MyCommandTextFormatter : ICommandTextFormatter +{ + public string FormatCommandUsage(string commandName, string? group, CommandParameterInfo[] parameters, bool includeSlash = true) + { + var prefix = includeSlash ? "/" : ""; + var groupPrefix = group != null ? $"{group} " : ""; + var paramStr = string.Join(" ", parameters.Select(p => + p.IsOptional ? $"[{p.Name}]" : $"<{p.Name}>" + )); + + return $"{prefix}{groupPrefix}{commandName} {paramStr}".Trim(); + } +} +``` + +Register your custom formatter in your startup class to replace the default: + +```csharp +services.RemoveAll(typeof(ICommandTextFormatter)); +services.AddSingleton(); +``` + +### Custom Parameter Type Parsing + +To support parsing of custom parameter types, implement . The recommended approach is to extend and override the `CreateParser` method to handle your custom types: + +```csharp +public class MyCommandParameterParserFactory : DefaultCommandParameterParserFactory +{ + public override ICommandParameterParser? CreateParser(ParameterInfo[] parameters, int index) + { + var param = parameters[index]; + var paramType = param.ParameterType; + + // Handle custom type + if (paramType == typeof(Vector3)) + { + return new Vector3Parser(); + } + + // Fall back to default types + return base.CreateParser(parameters, index); + } +} +``` + +Register your custom parser factory in your startup class: + +```csharp +services.RemoveAll(typeof(ICommandParameterParserFactory)); +services.AddSingleton(); +``` + +Your custom parser receives the raw input string and should return a parsed value. diff --git a/docs/support/platform-support.md b/docs/support/platform-support.md new file mode 100644 index 0000000..324e91b --- /dev/null +++ b/docs/support/platform-support.md @@ -0,0 +1,72 @@ +--- +title: Platform & Version Support +uid: platform-support +--- + +# Platform & Version Support + +SampSharp comes in two versions, each designed for different server platforms and with different capabilities: + +- **SampSharp v1.x (for open.mp)** — A modern rewrite built from the ground up for the open.mp server. It embraces current .NET technologies, requires 64-bit architecture, and introduces a powerful ECS (Entity Component System) framework alongside the traditional gamemode framework. + +- **SampSharp v0.x (Legacy)** — The original implementation targeting SA-MP servers. While it continues to receive maintenance and bug fixes, it is no longer the primary development focus and has inherent architectural limitations. + +## Why SampSharp v1.x? + +SampSharp v1.x was created to overcome fundamental limitations in the legacy codebase and to take advantage of modern server technology: + +### Key Limitations of Legacy (v0.x) + +> [!IMPORTANT] +> The legacy version is tied to 32-bit architecture and older .NET runtimes, which create several constraints: +> +> - **Outdated .NET versions**: Locked to .NET versions that Microsoft no longer actively supports +> - **32-bit Linux unsupported**: Microsoft does not officially support .NET on 32-bit (x86) Linux; only unsupported community builds available + +### Advantages of v1.x + +> [!TIP] +> SampSharp v1.x delivers: +> +> - **open.mp integration**: Built from the ground up to leverage open.mp's component extensibility platform, providing seamless integration with native open.mp features and future updates +> - **Modern .NET support**: Full support for latest .NET LTS releases (11, 12, etc.) on Windows and Linux +> - **Better Linux support**: Reliable .NET runtime on Linux x86_64 +> - **Cleaner architecture**: Built with modern C# and design patterns for easier maintenance and feature development + +## Support Matrix + +| Platform | v1.x | v0.x (Legacy) | +|----------|------|---------------| +| **SA-MP Server** — Windows | ✗ Unsupported | ✓ Full Support | +| **SA-MP Server** — Linux | ✗ Unsupported | ◐ Partial Support † | +| **open.mp Server** — x86 (32-bit) | ◐ Coming Soon | ◐ Partial Support † | +| **open.mp Server** — x86_64 (64-bit) | ✓ Full Support | ✗ Unsupported | +| **Latest .NET Version** — Windows | ✓ Yes | ✓ Yes | +| **Latest .NET Version** — Linux | ✓ Yes | ✗ No † | +| **Gamemode Framework** | ✗ Not Planned | ✓ Yes | +| **ECS Framework** | ✓ Yes | ✓ Yes | + +### Legend + +- **✓ Full Support** — Fully supported and tested +- **◐ Partial Support** — Supported but with limitations or workarounds +- **✗ Unsupported** — Not supported in this version + +### Footnotes + +† **32-bit Linux and Unofficial Runtimes** + +Microsoft does not officially support .NET on 32-bit (x86) Linux. For legacy SampSharp on x86 Linux or open.mp x86 servers, users must rely on community-built .NET 6 or .NET 8 runtimes. While functional in many cases, these unofficial builds contain bugs and the .NET 8 builds are known to be unstable. This is not a limitation of SampSharp itself, but rather the platform constraints of 32-bit Linux environments. + +## Migration from Legacy + +If you're currently using SampSharp v0.x and considering upgrading to v1.x, see the for detailed instructions on how to port your gamemode to the new architecture. + +## Getting Started + +Ready to start with SampSharp v1.x? + +- +- + +For legacy support and documentation, see the section. diff --git a/docs/support/toc.yml b/docs/support/toc.yml index cc95b6e..8093838 100644 --- a/docs/support/toc.yml +++ b/docs/support/toc.yml @@ -1,3 +1,4 @@ items: -- href: troubleshooting.md +- href: platform-support.md - href: migration-guide.md +- href: troubleshooting.md