Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/core-concepts/entities-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ In SampSharp, an **entity** is a unique object in the game world, such as a play
Entities and components are the building blocks of the ECS architecture:
- Entities are identified by an `EntityId` (<xref:SampSharp.Entities.EntityId>)
- Components are subclasses of <xref:SampSharp.Entities.Component>
- Systems operate on entities by querying for specific component combinations
- Systems operate on entities by handling events; the dispatcher resolves the relevant components from the involved entities and passes them to the handler

## Creating Entities

Expand Down Expand Up @@ -112,6 +112,25 @@ You can also destroy a single component:
component.Destroy();
```

## Component Liveness

Once a component is destroyed — directly via `Destroy()`, indirectly via `DestroyEntity()`, or when its underlying game object goes away (for example, when a player disconnects) — the C# object remains in memory until the garbage collector reclaims it, but calling methods or accessing properties that touch the underlying native handle will throw `ObjectDisposedException`.

If your code holds onto a component across a boundary where it could have been destroyed in the meantime — typically across `await`, a timer callback, or a captured closure — check liveness before using it:

```csharp
[Event]
public async Task OnPlayerConnect(Player player)
{
await SomeLongRunningWorkAsync();

if (player)
player.SendClientMessage("Welcome back to your seat.");
}
```

Every component is implicitly truthy when alive and falsy when destroyed or `null`, so `if (component)` is enough. The underlying flag is also exposed as `component.IsComponentAlive` if you need to read it explicitly. Inside `OnDestroyComponent`, the related `IsDestroying` property is `true` — useful for distinguishing the destruction pass from normal operation.

## Working with Components from a Component

From any <xref:SampSharp.Entities.Component>, you can:
Expand Down
146 changes: 146 additions & 0 deletions docs/core-concepts/startup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
title: Startup and configuration
uid: startup
---

# Startup and configuration

Every SampSharp gamemode begins with a `Startup` class that implements <xref:SampSharp.Entities.IEcsStartup>. SampSharp creates an instance of this class when the gamemode loads and uses it to wire up the ECS framework, register services into the DI container, and configure event handling.

The project template generates a minimal startup:

```csharp
using Microsoft.Extensions.DependencyInjection;
using SampSharp.Entities;
using SampSharp.Entities.SAMP.Commands;
using SampSharp.OpenMp.Core;

public class Startup : IEcsStartup
{
public void Initialize(IStartupContext context)
{
context.UseEntities().UseCommands();
}

public void ConfigureServices(IServiceCollection services)
{
}

public void Configure(IEcsBuilder builder)
{
}
}
```

The three methods run at different points during startup:

| Method | Runs | Used for |
|---|---|---|
| `Initialize(IStartupContext)` | Earliest. Has access to the open.mp host. | Calling `UseEntities()` and adding feature modules (`UseCommands`, custom hosts). |
| `ConfigureServices(IServiceCollection)` | After the service collection is created, before the provider is built. | Adding your own services to dependency injection. |
| `Configure(IEcsBuilder)` | After the service provider is built, just before `OnGameModeInit` fires. | Final pre-launch work that needs resolved services — preloading data, warming up caches, kicking off background services. |

## Initialize and the ECS host builder

`Initialize` is where you opt into the ECS framework by calling `UseEntities()` on the startup context. The call returns an <xref:SampSharp.Entities.IEcsHostBuilder> on which you chain everything else:

```csharp
public void Initialize(IStartupContext context)
{
context.UseEntities()
.UseCommands()
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information))
.ConfigureUnhandledExceptionHandler((sp, where, ex) =>
{
var log = sp.GetRequiredService<ILogger<Startup>>();
log.LogError(ex, "Unhandled exception in {Where}", where);
});
}
```

The host builder supports the following configuration:

- **`Configure(Action<IEcsBuilder>)`** — schedules a callback that runs after the service provider is built, just before `OnGameModeInit` fires. The same hook as the `Configure` method on `IEcsStartup`, but exposed on the host builder so a feature module can register its own pre-launch work.
- **`ConfigureServices(Action<IServiceCollection>)`** — register services. Useful when a feature module wants to add its own services on top of yours. There's also an overload that exposes the `SampSharpEnvironment` if you need it.
- **`ConfigureLogging(Action<ILoggingBuilder>)`** — set log levels, add custom providers (Serilog, file logging, etc.). open.mp's console logger is added automatically.
- **`ConfigureUnhandledExceptionHandler(UnhandledExceptionHandler)`** — replace the default handler for uncaught exceptions thrown from event handlers, systems, timers, etc. The default writes the exception to the configured logger; override it to forward to an error tracker or take other action.
- **`UseServiceProviderFactory<T>(IServiceProviderFactory<T>)`** — swap out the default Microsoft DI container for an alternative such as Autofac, Lamar, or DryIoc.
- **`DisableDefaultSystemsLoading()`** — by default SampSharp scans the entry assembly and registers every `ISystem` it finds. Call this to opt out and register systems manually with `services.AddSystem<T>()`.

## Feature modules

`UseEntities()` returns the host builder, which can then be extended by feature modules. SampSharp ships with one for the [command system](xref:commands):

```csharp
context.UseEntities()
.UsePlayerCommands() // player /commands only
.UseConsoleCommands() // server console commands only
.UseCommands(); // shortcut for both
```

Each `UseXxx` extension is responsible for registering its own services and pre-launch work against the host builder, so you don't have to know the internals — just opt in to what your gamemode needs.

You can write your own modules the same way:

```csharp
public static class MyFeatureExtensions
{
extension(IEcsHostBuilder hostBuilder)
{
public IEcsHostBuilder UseMyFeature() => hostBuilder
.ConfigureServices(services => services.AddSingleton<IMyService, MyService>())
.Configure(builder => builder.Services.GetRequiredService<IMyService>().Warmup());
}
}
```

## ConfigureServices

The `ConfigureServices(IServiceCollection)` method on `IEcsStartup` is where you register your own services for [dependency injection](xref:systems#dependency-injection) into systems and event handlers:

```csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IBankService, BankService>();
services.AddSingleton<IPersistenceService, SqlitePersistenceService>();
services.AddDbContext<GameDbContext>(o => o.UseSqlite("Data Source=game.db"));
}
```

Inside the call, you also have access to the same `IServiceCollection` methods that any ASP.NET Core or generic-host app uses, so anything published on NuGet that expects `IServiceCollection` (logging providers, configuration, EF Core, etc.) will work.

> [!NOTE]
> `AddSystem<T>` is what registers a system with SampSharp's system registry. Systems are picked up automatically from the entry assembly, so you usually don't need to call this — only if you've disabled default loading or want to register a system from a different assembly.

## Configure (pre-launch hook)

`Configure(IEcsBuilder)` runs after the service provider has been built but **before** `OnGameModeInit` fires. It's the last chance to do startup work that needs resolved services — the rest of the gamemode hasn't run yet, so anything you do here is in place by the time event handlers and systems start receiving callbacks.

`IEcsBuilder.Services` exposes the fully-built `IServiceProvider`. Typical uses:

```csharp
public void Configure(IEcsBuilder builder)
{
// Run a database migration before the gamemode starts accepting events.
var db = builder.Services.GetRequiredService<GameDbContext>();
db.Database.Migrate();

// Pre-load reference data into a cache so the first OnPlayerConnect doesn't pay the cost.
var cache = builder.Services.GetRequiredService<IReferenceDataCache>();
cache.Preload();
}
```

If you don't have any pre-launch work to do, leaving this method empty is fine — the template generates it that way.

## What's wired up by default

When you call `UseEntities()`, SampSharp automatically registers a baseline of services and systems. You can rely on these being available without configuring anything:

- **Core infrastructure** — <xref:SampSharp.Entities.IEntityManager>, <xref:SampSharp.Entities.IEventDispatcher>, <xref:SampSharp.Entities.ISystemRegistry>, and an `IUnhandledExceptionHandler`.
- **Built-in systems** — `TimerSystem` (exposed as <xref:SampSharp.Entities.ITimerService>) and `TickingSystem`.
- **SAMP services** — <xref:SampSharp.Entities.SAMP.IWorldService>, <xref:SampSharp.Entities.SAMP.IServerService>, <xref:SampSharp.Entities.SAMP.IDialogService>, <xref:SampSharp.Entities.SAMP.INpcService>, plus all the open.mp event handlers that translate native callbacks into SampSharp events.
- **Logging** — `ILogger<T>` backed by open.mp's console logger. Add more providers via `ConfigureLogging`.
- **Auto-discovered systems** — every public `ISystem` type in the entry assembly, unless you call `DisableDefaultSystemsLoading()`.

Feature modules (like `UseCommands`) layer on top of this baseline.
124 changes: 124 additions & 0 deletions docs/features/gang-zones.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
title: Gang zones
uid: gang-zones
---

# Gang zones

A gang zone is a 2D rectangular overlay on a player's radar (and, when shown, a coloured square on the world map). They are commonly used to mark turf, mission areas, safe zones, or capture points. SampSharp distinguishes between **global** zones visible to any subset of players (<xref:SampSharp.Entities.SAMP.GangZone>) and **per-player** zones bound to a specific owner (<xref:SampSharp.Entities.SAMP.PlayerGangZone>). Both share the same surface via the common base <xref:SampSharp.Entities.SAMP.BaseGangZone>.

> [!NOTE]
> Creating a gang zone does not make it visible. You must call `Show()` (for everyone) or `Show(player)` for it to appear on the radar.

## Creating a gang zone

Use <xref:SampSharp.Entities.SAMP.IWorldService.CreateGangZone*> with the minimum and maximum corners of the rectangle:

```csharp
[Event]
public void OnGameModeInit(IWorldService worldService)
{
var zone = worldService.CreateGangZone(
min: new Vector2(2000, -1700),
max: new Vector2(2100, -1600));

zone.Color = new Color(255, 0, 0, 128); // semi-transparent red
zone.Show(); // make it visible to all players
}
```

The boundary is updatable at runtime via `SetPosition(min, max)`.

## Showing, hiding, and flashing

Gang zones are visible per-player. `Show()` / `Hide()` apply to every connected player; the overloads taking a `Player` operate on one player only:

```csharp
zone.Show(); // show to everyone
zone.Show(player); // show to a single player
zone.Hide(player); // hide from a single player

zone.Flash(Color.Yellow); // flash for everyone
zone.Flash(player, Color.Yellow); // flash for one player
zone.StopFlash(player); // stop flashing for one player
```

Per-player state is readable too: `IsShownForPlayer(player)`, `IsFlashingForPlayer(player)`, `GetColorForPlayer(player)`, `GetFlashingColorForPlayer(player)`. Use `GetShownFor()` to enumerate every player who currently sees the zone.

## Player gang zones

A <xref:SampSharp.Entities.SAMP.PlayerGangZone> is the same kind of overlay, but bound to a single owner via <xref:SampSharp.Entities.SAMP.IWorldService.CreatePlayerGangZone*>:

```csharp
[Event]
public void OnPlayerSpawn(Player player, IWorldService worldService)
{
var personal = worldService.CreatePlayerGangZone(
owner: player,
min: new Vector2(2000, -1500),
max: new Vector2(2020, -1480));

personal.Color = new Color(0, 255, 0, 128);
personal.Show(player);
}
```

`PlayerGangZone` is not automatically destroyed when the owner disconnects — pass `parent: player` if you want the zone to disappear alongside the player entity:

```csharp
worldService.CreatePlayerGangZone(owner: player, min: a, max: b, parent: player);
```

## Enter / leave tracking

Gang zone enter and leave events are **opt-in** — they do not fire by default. Register the zone for containment checking via <xref:SampSharp.Entities.SAMP.IWorldService.UseGangZoneCheck*>:

```csharp
public class TerritorySystem : ISystem
{
private readonly GangZone _zone;

public TerritorySystem(IWorldService world)
{
_zone = world.CreateGangZone(new Vector2(2000, -1700), new Vector2(2100, -1600));
_zone.Color = new Color(255, 0, 0, 128);
_zone.Show();

world.UseGangZoneCheck(_zone, enable: true); // start firing enter/leave events
}

[Event]
public void OnPlayerEnterGangZone(Player player, GangZone zone)
{
if (zone == _zone)
player.SendClientMessage(Color.Red, "Entered enemy territory.");
}

[Event]
public void OnPlayerLeaveGangZone(Player player, GangZone zone)
{
if (zone == _zone)
player.SendClientMessage(Color.White, "You are safe.");
}
}
```

Per-player zones dispatch under different event names: `OnPlayerEnterPlayerGangZone(Player, PlayerGangZone)` and `OnPlayerLeavePlayerGangZone(Player, PlayerGangZone)`.

`BaseGangZone.IsPlayerInside(player)` returns the current containment state, but only for zones registered with `UseGangZoneCheck` — otherwise it is always `false`.

## Click events

When a player clicks on a gang zone on the world map, `OnPlayerClickGangZone` (or `OnPlayerClickPlayerGangZone` for player-scoped zones) fires. This requires nothing special beyond the zone being shown:

```csharp
[Event]
public void OnPlayerClickGangZone(Player player, GangZone zone)
{
player.SendClientMessage($"You clicked zone {zone}.");
}
```

## Lifetime

A `GangZone` or `PlayerGangZone` is destroyed when you call `Destroy()` on it, when its parent entity is destroyed, or when the server shuts down. A `PlayerGangZone` is **not** implicitly tied to its owner's lifetime — see [Player gang zones](#player-gang-zones) above for parenting it to the player if you want that. As with any component, holding the reference across an `await` or timer callback can yield a destroyed instance — guard with `if (zone)` before use. See [Component liveness](xref:entities-components#component-liveness) for the full explanation.
Loading
Loading