diff --git a/docs/core-concepts/entities-components.md b/docs/core-concepts/entities-components.md index af5415e..d9f04a0 100644 --- a/docs/core-concepts/entities-components.md +++ b/docs/core-concepts/entities-components.md @@ -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` () - Components are subclasses of -- 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 @@ -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 , you can: diff --git a/docs/core-concepts/startup.md b/docs/core-concepts/startup.md new file mode 100644 index 0000000..c9177c8 --- /dev/null +++ b/docs/core-concepts/startup.md @@ -0,0 +1,146 @@ +--- +title: Startup and configuration +uid: startup +--- + +# Startup and configuration + +Every SampSharp gamemode begins with a `Startup` class that implements . 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 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>(); + log.LogError(ex, "Unhandled exception in {Where}", where); + }); +} +``` + +The host builder supports the following configuration: + +- **`Configure(Action)`** — 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)`** — 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)`** — 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(IServiceProviderFactory)`** — 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()`. + +## 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()) + .Configure(builder => builder.Services.GetRequiredService().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(); + services.AddSingleton(); + services.AddDbContext(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` 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(); + db.Database.Migrate(); + + // Pre-load reference data into a cache so the first OnPlayerConnect doesn't pay the cost. + var cache = builder.Services.GetRequiredService(); + 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** — , , , and an `IUnhandledExceptionHandler`. +- **Built-in systems** — `TimerSystem` (exposed as ) and `TickingSystem`. +- **SAMP services** — , , , , plus all the open.mp event handlers that translate native callbacks into SampSharp events. +- **Logging** — `ILogger` 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. diff --git a/docs/features/gang-zones.md b/docs/features/gang-zones.md new file mode 100644 index 0000000..b1fc8dd --- /dev/null +++ b/docs/features/gang-zones.md @@ -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 () and **per-player** zones bound to a specific owner (). Both share the same surface via the common base . + +> [!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 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 is the same kind of overlay, but bound to a single owner via : + +```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 : + +```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. diff --git a/docs/features/npcs.md b/docs/features/npcs.md new file mode 100644 index 0000000..7e7d85a --- /dev/null +++ b/docs/features/npcs.md @@ -0,0 +1,260 @@ +--- +title: NPCs +uid: npcs +--- + +# NPCs + +NPCs are server-controlled bots that share the world with real players. SampSharp exposes them through the component and the method — you build and drive them entirely from your gamemode. + +For lighter-weight characters that just stand around — shop clerks, ambient bystanders, animated background figures — SampSharp also exposes , a static non-movable character with a much smaller API. See [Actors](#actors) at the end of the article. + +> [!NOTE] +> SampSharp also exposes the older SA-MP NPC mechanism (`IServerService.ConnectNpc`), but it has been **deprecated** by open.mp. New gamemodes should use the open.mp NPC system covered here. See [Legacy SA-MP NPCs](#legacy-sa-mp-npcs) at the end of the article if you still need to interact with one. + +### NPC or Actor? + +Pick the lightest option that fits the role: + +| Need | Use | +|---|---| +| A character that just stands, looks around, or plays an animation | | +| A character that walks, drives, shoots, follows paths, or replays a recording | | + +## Creating an NPC + +Use to spawn an NPC. The returned is its own entity and is **not** a `Player`: + +```csharp +[Event] +public void OnGameModeInit(IWorldService worldService) +{ + var npc = worldService.CreateNpc("Bandit"); + npc.Skin = 109; + npc.Position = new Vector3(2000, -1500, 13); + npc.Spawn(); +} +``` + +Like other entities, NPCs accept a `parent` argument so they can be cleaned up by destroying the parent — useful for tying bots to a round, mission, or instance. + +## Moving an NPC + +The component exposes several ways to drive movement, each picking a different trade-off between simplicity and control: + +```csharp +// Walk/jog/sprint/drive to a fixed point +npc.MoveTo(new Vector3(2010, -1500, 13), NPCMoveType.Jog); + +// Follow a player, recalculating their position every 500 ms +npc.MoveToPlayer(player, NPCMoveType.Sprint); + +// Stop whatever the NPC is currently doing +npc.StopMoving(); +``` + +`IsMoving`, `PositionMovingTo`, and `Velocity` let you inspect the current state. + +### Paths + +A path is a reusable list of waypoints. Build one through , then tell an NPC to follow it: + +```csharp +public class PatrolSystem : ISystem +{ + public PatrolSystem(INpcService npc, IWorldService world) + { + var pathId = npc.CreatePath(); + npc.AddPointToPath(pathId, new Vector3(2000, -1500, 13), stopRange: 1f); + npc.AddPointToPath(pathId, new Vector3(2050, -1500, 13), stopRange: 1f); + npc.AddPointToPath(pathId, new Vector3(2050, -1450, 13), stopRange: 1f); + + var bandit = world.CreateNpc("Patrol_01"); + bandit.Spawn(); + bandit.MoveByPath(pathId, NPCMoveType.Jog); + } + + [Event] + public void OnNPCFinishMovePath(Npc npc, int pathId) + { + // Restart the patrol + npc.MoveByPath(pathId, NPCMoveType.Jog, reverse: true); + } +} +``` + +Use `PausePath`, `ResumePath`, and `StopPath` to control playback. + +### Nodes + +Nodes are the in-game pedestrian/vehicle node files shipped with GTA: San Andreas. They let an NPC navigate the world using the same network the AI uses. Open a node file with `INpcService.OpenNode`, then start an NPC on it: + +```csharp +npcService.OpenNode(0); // pedestrian nodes for the first node file +npc.PlayNode(0, NPCMoveType.Jog); +``` + +`OnNPCFinishNodePoint` and `OnNPCFinishNode` fire as the NPC traverses the network. + +## Combat + +NPCs can aim at and shoot players, vehicles, and other NPCs: + +```csharp +npc.Weapon = (byte)Weapon.MP5; +npc.Ammo = 200; + +npc.AimAtPlayer(target, + shoot: true, + shootDelay: 200, + setAngle: true, + offset: default, + offsetFrom: default, + betweenCheckFlags: EntityCheckType.None); + +// Tune accuracy per weapon (0.0 - 1.0) +npc.SetWeaponAccuracy((byte)Weapon.MP5, 0.5f); +``` + +`StopAim`, `Shoot`, `MeleeAttack`, `EnableInfiniteAmmo`, and `EnableReloading` cover the rest of the combat surface. + +## Vehicles + +NPCs can drive vehicles. `EnterVehicle` makes the NPC walk to the vehicle and get in; `PutInVehicle` teleports them directly into a seat: + +```csharp +var car = worldService.CreateVehicle(VehicleModelType.Sultan, npc.Position, 0f, 1, 1); + +npc.PutInVehicle(car, seat: 0); +npc.MoveTo(new Vector3(2200, -1700, 13), NPCMoveType.Drive); +``` + +When driving, properties like `VehicleHealth`, `IsVehicleSirenUsed`, and `VehicleGearState` operate on the NPC's current vehicle. + +## Recordings (playback) + +A recording is a `.rec` file produced by . The NPC can replay one to reproduce a player's exact movement — useful for scripted sequences, race ghosts, or canned animations: + +```csharp +// Play back a file directly +npc.StartPlayback("missions/intro_drive.rec"); + +// Or preload it once and reuse the record ID +var recordId = npcService.LoadRecord("missions/intro_drive.rec"); +npc.StartPlayback(recordId, autoUnload: false); +``` + +`OnNPCPlaybackStart` and `OnNPCPlaybackEnd` notify you when playback begins and finishes. `PausePlayback` and `StopPlayback` control it mid-flight. + +## Events + +NPC events follow the same pattern as other events — declare a handler with `[Event]` in a system. A few of the most useful: + +```csharp +public class NpcEventSystem : ISystem +{ + [Event] + public void OnNPCSpawn(Npc npc) { /* ... */ } + + [Event] + public void OnNPCDeath(Npc npc, Player killer, int reason) { /* ... */ } + + [Event] + public void OnNPCFinishMove(Npc npc) { /* arrived at MoveTo target */ } + + [Event] + public bool OnNPCTakeDamage(Npc npc, Player from, float amount, Weapon weapon, BodyPart part) + { + // Return false to reject the damage + return true; + } +} +``` + +See for the full list of NPC events. + +## Lifetime + +An `Npc` is destroyed when you call `Destroy()` on the component, when the parent entity is destroyed, or when the server shuts down. As with any component, holding the reference across an `await` or timer callback can yield a destroyed instance — guard with `if (npc)` before use. See [Component liveness](xref:entities-components#component-liveness) for the full explanation. + +## Actors + +An is a static, non-movable character — it has a skin, a position, a facing angle, health, and can play animations, but cannot walk, drive, or be controlled like an NPC. It's ideal for shop clerks, ambient pedestrians, mission-giver characters, or any visual humanoid that doesn't need behaviour. + +Create one through : + +```csharp +[Event] +public void OnGameModeInit(IWorldService worldService) +{ + var clerk = worldService.CreateActor( + modelId: 156, // skin + position: new Vector3(1352, -1758, 13), + rotation: 0f); + + clerk.IsInvulnerable = true; + clerk.ApplyAnimation( + library: "SHOP", + name: "SHP_Rob_React", + fDelta: 4.1f, + loop: true, lockX: false, lockY: false, freeze: false, + time: TimeSpan.Zero); +} +``` + +The full surface is small: `Skin`, `Health`, `IsInvulnerable`, `Angle`, plus the position/rotation/virtual world properties inherited from , and `ApplyAnimation` / `ClearAnimations`. See for the complete list. + +### Actor events + +```csharp +[Event] +public void OnActorStreamIn(Actor actor, Player forPlayer) { /* ... */ } + +[Event] +public void OnActorStreamOut(Actor actor, Player forPlayer) { /* ... */ } + +[Event] +public void OnPlayerGiveDamageActor(Player player, Actor actor, float amount, Weapon weapon, BodyPart part) +{ + // Actors don't die — they just absorb damage unless you make them invulnerable. +} +``` + +### Damage and invulnerability + +By default, actors are **vulnerable** — they can be shot, set on fire, run over, and so on. Damage works differently from players in three important ways: + +- Damage **does not automatically subtract from `Health`**. The server raises `OnPlayerGiveDamageActor` with the damage amount, and your code is responsible for applying it (typically `actor.Health -= amount;`). +- Actors **do not have a death state**. When `Health` reaches zero they keep standing (or playing whatever animation they had); there is no automatic ragdoll, no kill feed, and no "death" event. If you want an actor to fall over or disappear when killed, you implement that yourself — usually by playing a death animation and then calling `actor.Destroy()`. +- Setting `actor.IsInvulnerable = true` stops `OnPlayerGiveDamageActor` from firing for that actor. The change only takes effect once the actor is restreamed to each player, so set it on creation if possible. Setting it later may not visibly apply until the actor leaves and re-enters a player's stream radius. + +```csharp +[Event] +public void OnPlayerGiveDamageActor(Player player, Actor actor, float amount, Weapon weapon, BodyPart part) +{ + actor.Health -= amount; + + if (actor.Health <= 0f) + { + actor.ApplyAnimation("PED", "KO_shot_front", 4.1f, false, false, false, true, TimeSpan.FromSeconds(3)); + actor.Destroy(); + } +} +``` + +## Legacy SA-MP NPCs + +> [!WARNING] +> The legacy NPC path (`ConnectNpc` / `samp-npc` / Pawn scripts) is **deprecated** by open.mp. Use the open.mp NPC system covered above for any new bots, and consider porting existing legacy NPCs over. + +A legacy NPC is a separate `samp-npc` client process that connects to the server and runs a Pawn script located in the `npcmodes/` folder. The C# side launches the bot with : + +```csharp +[Event] +public void OnGameModeInit(IServerService server) +{ + server.ConnectNpc(name: "Bot_01", script: "idle"); // npcmodes/idle.amx +} +``` + +Once connected, the NPC appears in the world as an ordinary player. It is exposed as a component with set to `true`, and it goes through the normal `OnPlayerConnect`, `OnPlayerSpawn`, etc. flow. Movement and behaviour are driven entirely by the Pawn script — your C# code cannot tell the NPC what to do beyond what's available on a regular `Player` (skin, position, weapon, etc.). diff --git a/docs/features/objects.md b/docs/features/objects.md index 40a9621..dabc620 100644 --- a/docs/features/objects.md +++ b/docs/features/objects.md @@ -19,7 +19,7 @@ public void OnGameModeInit(IWorldService worldService) modelId: 18631, // object model ID position: new Vector3(100, 200, 30), // position rotation: new Vector3(0, 0, 45), // rotation - drawDistance: 300 // draw distance + drawDistance: 300 // override; pass 0 to use the engine default ); } ``` @@ -28,6 +28,9 @@ The returned component is of type (n See for all available parameters. +> [!NOTE] +> The base game has a hard cap of 1000 simultaneous global objects (`MAX_OBJECTS`). If you regularly need more, scope objects per player with `CreatePlayerObject` (also 1000 per player) or look into a streamer plugin. + ## Player Objects Player objects are only visible to a specific player, making them useful for personalized or player-specific world elements. Create player objects using : @@ -72,3 +75,7 @@ obj.Rotation = new Vector3(0, 0, 90); // Change rotation See for the full API. +## Lifetime + +A `GlobalObject` or `PlayerObject` is destroyed when you call `Destroy()` on it, when its parent entity is destroyed (a `PlayerObject` is destroyed when its owning player disconnects), or when the server shuts down. As with any component, holding the reference across an `await` or timer callback can yield a destroyed instance — guard with `if (obj)` before use. See [Component liveness](xref:entities-components#component-liveness) for the full explanation. + diff --git a/docs/features/pickups.md b/docs/features/pickups.md new file mode 100644 index 0000000..d5e55b0 --- /dev/null +++ b/docs/features/pickups.md @@ -0,0 +1,121 @@ +--- +title: Pickups +uid: pickups +--- + +# Pickups + +A pickup is a static collectible placed in the world — health and armor packs, weapons, scripted markers, and so on. SampSharp distinguishes between **global** pickups visible to everyone () and **per-player** pickups visible to (and collectible by) only their owner (). Both components share the same surface, exposed by their common base . + +## Creating a pickup + +Use for a pickup visible to every player: + +```csharp +[Event] +public void OnGameModeInit(IWorldService worldService) +{ + var pickup = worldService.CreatePickup( + model: 1240, // health pack + type: PickupType.ShowAndRespawnWhenDeath, + position: new Vector3(2000, -1500, 13)); +} +``` + +Every pickup carries a , which dictates its respawn and visibility behaviour — e.g. `ShowAndRespawnWhenDeath` reappears each time the picking player dies, `ShowNearAndRespawnWhenPickup` reappears after 30 seconds if no player is nearby, and `ShowButNotPickupable` is purely decorative. Some types also restrict how the pickup can be collected (e.g. `ShowAndPickupableWithVehicleWithSound` is vehicle-only). See for the full list with behaviour notes. + +> [!NOTE] +> Pickups placed outside the world coordinate range of -4096.0 to 4096.0 (on the X or Y axis) will not display and will not fire pickup events. + +### Static pickups + + creates a pickup the server handles automatically — weapon, health, and armor models give their effect to the player on contact without you wiring up an event: + +```csharp +worldService.CreateStaticPickup( + model: 1242, // armor + type: PickupType.ShowAndRespawnWhenDeath, + position: new Vector3(2010, -1500, 13)); +``` + +Static pickups are "set and forget": they cannot be destroyed individually and do not fire `OnPlayerPickUpPickup`. Use them for simple resupply points and reach for regular `CreatePickup` whenever you need to react to the pickup, mutate it later, or destroy it. + +## Player pickups + +A is created the same way but bound to a single owner via : + +```csharp +[Event] +public void OnPlayerSpawn(Player player, IWorldService worldService) +{ + worldService.CreatePlayerPickup( + owner: player, + model: 1254, // money bag + type: PickupType.ShowTillPickedUp, + position: player.Position + new Vector3(2, 0, 0)); +} +``` + +Under the hood, open.mp has no dedicated per-player pickup pool — `CreatePlayerPickup` creates a global pickup tagged with the owner, and only the owner sees and can collect it. One thing worth knowing: the pickup is **not** automatically destroyed when the owner disconnects. If you want it to disappear with the player, pass the player as the `parent` argument so the pickup entity is destroyed alongside the player entity: + +```csharp +worldService.CreatePlayerPickup( + owner: player, + model: 1254, + type: PickupType.ShowTillPickedUp, + position: player.Position, + parent: player); +``` + +## Handling pickup events + +When a player walks over a pickup, SampSharp dispatches one of two events depending on whether the pickup is bound to a player: + +```csharp +public class PickupSystem : ISystem +{ + [Event] + public void OnPlayerPickUpPickup(Player player, Pickup pickup) + { + if (pickup.Model == 1240) + player.Health = Math.Min(100f, player.Health + 25f); + } + + [Event] + public void OnPlayerPickUpPlayerPickup(Player player, PlayerPickup pickup) + { + player.GiveMoney(500); + pickup.Destroy(); + } +} +``` + +> [!NOTE] +> Static pickups created with `CreateStaticPickup` are handled by the server and do **not** fire these events. + +## Manipulating pickups + +`BasePickup` exposes a handful of properties and methods shared between `Pickup` and `PlayerPickup`: + +```csharp +// Change the appearance or behaviour at runtime +pickup.SetModel(1242); +pickup.SetType(PickupType.ShowNearAndRespawnWhenPickup); + +// Per-player visibility +pickup.SetHiddenForPlayer(player, hidden: true); +if (pickup.IsHiddenForPlayer(player)) { /* ... */ } + +// Force streaming +pickup.StreamOutForPlayer(player); +pickup.StreamInForPlayer(player); + +// Move silently (no visual update) +pickup.SetPositionNoUpdate(new Vector3(2020, -1500, 13)); +``` + +See for the full API. + +## Lifetime + +A `Pickup` or `PlayerPickup` is destroyed when you call `Destroy()` on it, when its parent entity is destroyed, or when the server shuts down. Note that a `PlayerPickup` is **not** implicitly tied to its owner's lifetime — see [Player pickups](#player-pickups) 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 (pickup)` before use. See [Component liveness](xref:entities-components#component-liveness) for the full explanation. diff --git a/docs/features/players.md b/docs/features/players.md index a5f909e..6865c25 100644 --- a/docs/features/players.md +++ b/docs/features/players.md @@ -3,17 +3,276 @@ title: Players uid: players --- -# Working with Players - -> [!NOTE] -> This article is coming soon! Check back later, or feel free to open an issue if you have questions. - - +# Players + +A connected player is represented by the component, which SampSharp attaches to the player's entity automatically when the client connects. You never create or destroy `Player` components yourself — the lifecycle is driven by the server. + +## Accessing players + +The most common way to obtain a `Player` is as a parameter on an [event handler](xref:events): + +```csharp +[Event] +public void OnPlayerConnect(Player player) +{ + player.SendClientMessage($"Welcome, {player.Name}!"); +} +``` + +To broadcast to every connected player, call rather than looping yourself: + +```csharp +worldService.SendClientMessage(Color.Yellow, "Round starting in 30 seconds."); +``` + +When you need to act on a subset of players — for example, only players on a team — enumerate via the entity manager and filter: + +```csharp +foreach (var player in entityManager.GetComponents()) +{ + if (player.Team == redTeam) + player.SendClientMessage(Color.Red, "Defend the flag!"); +} +``` + +## NPCs and players + +SampSharp has two NPC mechanisms, and only one of them produces a `Player`: + +- **`IServerService.ConnectNpc(name, script)`** — the **deprecated** legacy SA-MP path. The NPC connects to the server like any client and is exposed as a `Player`; returns `true` on it. +- **`IWorldService.CreateNpc(name)`** — the recommended open.mp path. Returns a separate component on its own entity. It is **not** a `Player`, and player events do not fire for it. + +If you have legacy NPCs and want a handler to run only for human players, filter on `IsNpc`: + +```csharp +[Event] +public void OnPlayerSpawn(Player player) +{ + if (player.IsNpc) + return; + + player.GiveMoney(1000); +} +``` + +See for details on both NPC mechanisms. + +## Handling player events + +SampSharp surfaces a wide range of player events. A few of the most commonly used: + +```csharp +public class PlayerEventSystem : ISystem +{ + [Event] + public void OnPlayerConnect(Player player) + { + player.SendClientMessage($"Hello, {player.Name}."); + } + + [Event] + public void OnPlayerDisconnect(Player player, DisconnectReason reason) + { + Console.WriteLine($"{player.Name} left ({reason})."); + } + + [Event] + public void OnPlayerSpawn(Player player) + { + player.GiveWeapon(Weapon.Colt45, 50); + } + + [Event] + public void OnPlayerDeath(Player player, Player killer, Weapon reason) + { + if (killer != null) + killer.Score++; + } + + [Event] + public bool OnPlayerText(Player player, string message) + { + // Return false to suppress the message; return true to let it propagate to chat. + return !message.Contains("badword", StringComparison.OrdinalIgnoreCase); + } +} +``` + +See for the full list of player events and their return-value semantics. + +## Class selection and spawning + +Before a player spawns, they go through class selection. The sequence of events is: + +1. `OnPlayerConnect` — the player joins the server. +2. `OnPlayerRequestClass` — fires every time the player scrolls to a different class at the class selection screen. +3. `OnPlayerRequestSpawn` — the player clicks "Spawn". Return `false` to reject the spawn. +4. `OnPlayerSpawn` — the player has spawned and is in the world. + +Define the classes shown on the class selection screen with , typically from `OnGameModeInit`. The call returns a component representing that entry, which you can hold onto if you want to mutate it later: + +```csharp +[Event] +public void OnGameModeInit(IServerService server) +{ + server.AddPlayerClass( + modelId: 0, // CJ skin + spawnPosition: new Vector3(1958, -2184, 13), + angle: 0f, + weapon1: Weapon.Colt45, weapon1Ammo: 100); +} +``` + +`OnPlayerRequestClass` receives the `Class` the player is currently looking at — use it to preview that class to the player (for example, point the camera at the spawn location, or display class-specific UI): + +```csharp +[Event] +public void OnPlayerRequestClass(Player player, Class klass) +{ + player.SendClientMessage(Color.White, $"Class skin {klass.Skin}, team {klass.Team}."); + player.CameraPosition = klass.Location + new Vector3(0, 5, 2); + player.SetCameraLookAt(klass.Location); +} +``` + +Because a `Class` is just a component on its own entity, you can attach your own [custom components](xref:entities-components) to it to carry metadata that doesn't fit on the built-in `Class` — faction info, descriptions, perks, etc. — and read it back during class selection: + +```csharp +public class Faction : Component +{ + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; +} + +public class ClassSetupSystem : ISystem +{ + [Event] + public void OnGameModeInit(IServerService server) + { + var copClass = server.AddPlayerClass(modelId: 280, + spawnPosition: new Vector3(1552, -1675, 16), angle: 90f); + copClass.AddComponent().Name = "LSPD"; + + var medicClass = server.AddPlayerClass(modelId: 274, + spawnPosition: new Vector3(1172, -1323, 15), angle: 0f); + var medicFaction = medicClass.AddComponent(); + medicFaction.Name = "Paramedic"; + medicFaction.Description = "Revive downed players for a reward."; + } + + [Event] + public void OnPlayerRequestClass(Player player, Class klass) + { + var faction = klass.GetComponent(); + if (faction != null) + player.GameText($"~y~{faction.Name}", TimeSpan.FromSeconds(2), GameTextStyle.Style3); + } +} +``` + +To change a class's spawn data at runtime, call . To override spawn data for a specific player (for example, after they log in), call : + +```csharp +player.SetSpawnInfo( + team: 0, + skin: 26, + position: new Vector3(0, 0, 5), + rotation: 0f, + weapon1: Weapon.Deagle, weapon1Ammo: 50); +``` + +Use `player.ForceClassSelection()` to send a player back to the class selection screen (they will not actually return until they re-spawn, which you can trigger with `player.ToggleSpectating(true)` followed by `player.ToggleSpectating(false)`). + +## Manipulating players + +The `Player` component exposes properties and methods covering most things you'd want to do to a player. A small sampler: + +```csharp +// Identity and stats +player.SetName("NewName"); +player.Score = 100; +player.GiveMoney(500); + +// World state +player.Position = new Vector3(0, 0, 5); +player.SetPositionFindZ(new Vector3(1000, -1500, 0)); // snap to nearest ground +player.Interior = 1; +player.VirtualWorld = 42; + +// Health and combat +player.Health = 100f; +player.Armour = 50f; +player.GiveWeapon(Weapon.Deagle, 100); +player.SetArmedWeapon(Weapon.Deagle); +player.ResetWeapons(); + +// Vehicle interaction +player.PutInVehicle(vehicle, seatId: 0); +player.RemoveFromVehicle(); + +// UI and feedback +player.SendClientMessage(Color.Lime, "You picked up $500."); +player.GameText("~r~WASTED", TimeSpan.FromSeconds(3), GameTextStyle.Style2); +player.PlaySound(1057); + +// Animation +player.ApplyAnimation( + animationLibrary: "DANCING", + animationName: "dnce_M_b", + fDelta: 4.1f, + loop: true, lockX: false, lockY: false, freeze: false, + time: TimeSpan.Zero); +player.ClearAnimations(); + +// Moderation +player.Kick(); +player.Ban("Cheating"); +``` + +See for the full API. + +## Custom per-player state + +A player's entity is just an entity, so you can attach your own [components](xref:entities-components) to it — accounts, stats, login state, anything specific to your gamemode. They are destroyed automatically when the player disconnects, along with the `Player` component itself. + +```csharp +public class Account : Component +{ + public int UserId { get; set; } + public int Level { get; set; } + public bool IsLoggedIn { get; set; } +} + +public class AccountSystem : ISystem +{ + [Event] + public void OnPlayerConnect(Player player) + { + var account = player.AddComponent(); + account.Level = 1; + } + + [Event] + public void OnPlayerSpawn(Player player) + { + var account = player.GetComponent(); + if (account is { IsLoggedIn: false }) + player.SendClientMessage(Color.Red, "Please /login first."); + } +} +``` + +A handler can also receive a custom component directly as a parameter — the event dispatcher resolves it from the player's entity and skips the handler when the component is missing: + +```csharp +[Event] +public void OnPlayerText(Player player, string message, Account account) +{ + // Runs only for players that have an Account component attached. + account.Level++; +} +``` + +## Player lifetime + +A `Player` component is destroyed shortly after the player disconnects. If you hold the reference across an `await`, a timer callback, or any other boundary where the player may have left in the meantime, guard the access with `if (player)` before using it. See [Component liveness](xref:entities-components#component-liveness) for the full explanation. diff --git a/docs/features/text-draws.md b/docs/features/text-draws.md new file mode 100644 index 0000000..d04ff54 --- /dev/null +++ b/docs/features/text-draws.md @@ -0,0 +1,203 @@ +--- +title: Text draws +uid: text-draws +--- + +# Text draws + +A text draw is a 2D HUD element rendered on top of the screen — used for scoreboards, custom on-screen labels, menus, sprites, and clickable buttons. SampSharp distinguishes between **global** text draws shared across players () and **per-player** text draws (), each player drawing their own. + +> [!NOTE] +> Text draws are HUD overlays in screen space. For text rendered in the 3D world (above a player's head, on a sign, etc.) use instead. + +### Global or per-player? + +Pick the one that fits how the text draw is used: + +| Need | Use | +|---|---| +| One text draw shown to many players (a server logo, a shared event banner) | | +| Different text or values per player (HUDs, scoreboards with per-player stats) | | + +A global `TextDraw` can still show **different text to different players** via `SetTextForPlayer` — useful for changing one or two characters per player without exhausting the per-player text draw pool. + +## Creating a text draw + +Use with a 2D screen position and the initial text. Screen coordinates run from `(0, 0)` (top-left) to roughly `(640, 480)` (bottom-right) on a resolution-independent canvas — prefer whole numbers so the layout stays stable across player resolutions. + +```csharp +[Event] +public void OnGameModeInit(IWorldService worldService) +{ + var hud = worldService.CreateTextDraw( + position: new Vector2(10, 400), + text: "My Server"); + + hud.Font = TextDrawFont.Pricedown; + hud.LetterSize = new Vector2(0.5f, 1.6f); + hud.ForeColor = new Color(255, 255, 0, 255); + hud.UseBox = false; + hud.Show(); +} +``` + +`Show()` displays the text draw to every connected player. `Show(player)` and `Hide(player)` operate on one player only — the text draw is not visible until you show it. + +> [!NOTE] +> A newly created text draw has no inherent styling. If you do not set at least `Font` and `LetterSize`, the result may not render or may render as a thin line. Always configure the draw before calling `Show()`. + +> [!TIP] +> With `Alignment.Right`, the creation position is interpreted as the **top-right corner** instead of top-left. This is the same SA-MP quirk that affects [TextSize](#styling) — it shows up at create time too if you build right-aligned HUD elements. + +## Per-player text draws + +A is created the same way but only the owner sees it. Per-player draws are essential for HUDs that display different data for each player (health, money, ammo) since updating a global text draw's text changes it for everyone: + +```csharp +[Event] +public void OnPlayerSpawn(Player player, IWorldService worldService) +{ + var hud = worldService.CreatePlayerTextDraw( + player: player, + position: new Vector2(10, 400), + text: "Score: 0"); + + hud.Font = TextDrawFont.Normal; + hud.LetterSize = new Vector2(0.4f, 1.2f); + hud.ForeColor = new Color(255, 255, 255, 255); + hud.Show(); +} +``` + +A `PlayerTextDraw` is **not** automatically destroyed when the owner disconnects — pass `parent: player` if you want it to disappear with the player. + +## Styling + +Both `TextDraw` and `PlayerTextDraw` expose the same styling surface: + +```csharp +// Font face (Diploma, Normal, Slim, Pricedown, DrawSprite, PreviewModel) +draw.Font = TextDrawFont.Pricedown; + +// Letter width and height +draw.LetterSize = new Vector2(0.5f, 1.6f); + +// Colors +draw.ForeColor = new Color(255, 255, 255, 255); // letter color +draw.BackColor = new Color(0, 0, 0, 255); // background color +draw.Shadow = 1; // shadow depth +draw.Outline = 0; // outline thickness + +// Box around the text +draw.UseBox = true; +draw.BoxColor = new Color(0, 0, 0, 150); +draw.TextSize = new Vector2(200, 50); // box size + clickable area (see warning below) + +// Alignment (Default, Left, Center, Right) +draw.Alignment = TextDrawAlignment.Center; + +// Variable-width vs fixed-width characters +draw.Proportional = true; +``` + +> [!TIP] +> `LetterSize` X scales width, Y scales height. Most usable HUDs land around X 0.3–0.6 and Y 1.0–2.0. Increasing `Outline` makes text easier to read against busy backgrounds. + +> [!WARNING] +> `TextSize` interpretation depends on the current `Alignment` — this is an SA-MP quirk, not a SampSharp one: +> +> - **`Left`** — the `(x, y)` is the **right-most corner**, in absolute screen coordinates. +> - **`Center`** — `x` is the **overall box width**, and `x` and `y` must be **swapped** compared to the other alignments. +> - **`Right`** — the `(x, y)` is the **left-most corner**, in absolute screen coordinates. +> +> For `DrawSprite` and `PreviewModel` fonts, `TextSize` is instead the **width and height** of the rendered content (from the text draw's origin). +> +> If a box, hitbox, or sprite ends up the wrong size or in the wrong place, mis-aligned `TextSize` is almost always why. + +## Updating text + +Setting the `Text` property changes the text for everyone (or for the owner, on a `PlayerTextDraw`): + +```csharp +hud.Text = "Money: $1,000"; +``` + +For a **global** `TextDraw`, you can update the text shown to a single player without disturbing others using `SetTextForPlayer`. This is useful for shared HUD layouts where one or two numbers vary per player: + +```csharp +sharedHud.SetTextForPlayer(player, $"Score: {player.Score}"); +``` + +## Selectable (clickable) text draws + +Make a text draw clickable by enabling `Selectable`, setting an explicit `Alignment`, and giving it a `TextSize` whose value matches that alignment's [coordinate convention](#styling). Clicks only fire while the player is in text-draw selection mode: + +```csharp +public class MenuSystem : ISystem +{ + private readonly TextDraw _playButton; + + public MenuSystem(IWorldService world) + { + _playButton = world.CreateTextDraw(new Vector2(50, 25), "PLAY"); + _playButton.Font = TextDrawFont.Pricedown; + _playButton.LetterSize = new Vector2(0.8f, 2.2f); + _playButton.ForeColor = new Color(0, 255, 0, 255); + _playButton.UseBox = true; + + _playButton.Alignment = TextDrawAlignment.Left; + _playButton.TextSize = new Vector2(140, 20); + _playButton.Selectable = true; + } + + [Event] + public void OnPlayerConnect(Player player) + { + _playButton.Show(player); + + // Enter selection mode so the player can click; hoverColor highlights the + // currently-hovered text draw. + player.SelectTextDraw(new Color(255, 255, 0, 255)); + } + + [Event] + public void OnPlayerClickTextDraw(Player player, TextDraw draw) + { + if (draw != _playButton) + return; + + _playButton.Hide(player); + player.CancelSelectTextDraw(); + player.Spawn(); + } +} +``` + +A few things to note: + +- The text draw must be **shown to each player** (`Show(player)`) before they can click it — `Show()` only displays it to players who are connected at the time of the call, so new joiners won't see anything until you re-show it for them. +- Setting an explicit `Alignment` and a matching `TextSize` is required for the hitbox to register clicks. The exact relationship between position, alignment, and `TextSize` is fiddly in practice; the values above are a known-good starting point — adjust `TextSize` until the highlight on hover matches the visible box. +- Selection mode also enables the cursor. Call `player.CancelSelectTextDraw()` to exit, as shown above. If the player presses Escape themselves, `OnPlayerCancelTextDrawSelection` fires. +- Player text draws use the parallel `OnPlayerClickPlayerTextDraw` event. + +## Sprites and preview models + +Two specialised fonts unlock non-text content: + +- **`TextDrawFont.DrawSprite`** — renders a texture from a TXD library. Set the text to `"library:texture"` (for example, `"hud:radar_ammugun"`). +- **`TextDrawFont.PreviewModel`** — renders a 3D model preview inside the text draw box. Set `PreviewModel`, then `SetPreviewRotation(rotation, zoom)` to orient it. For vehicle previews, use `SetPreviewVehicleColor`. + +```csharp +var preview = worldService.CreateTextDraw(new Vector2(500, 200), "_"); +preview.Font = TextDrawFont.PreviewModel; +preview.TextSize = new Vector2(80, 80); +preview.UseBox = true; +preview.PreviewModel = 411; // Infernus +preview.SetPreviewRotation(new Vector3(0, 0, 45), zoom: 1.0f); +preview.SetPreviewVehicleColor(1, 1); +preview.Show(); +``` + +## Lifetime + +A `TextDraw` or `PlayerTextDraw` is destroyed when you call `Destroy()` on it, when its parent entity is destroyed, or when the server shuts down. A `PlayerTextDraw` is **not** implicitly tied to its owner's lifetime — pass `parent: player` to `CreatePlayerTextDraw` if you want the text draw to disappear when the player disconnects. As with any component, holding the reference across an `await` or timer callback can yield a destroyed instance — guard with `if (draw)` before use. See [Component liveness](xref:entities-components#component-liveness) for the full explanation. diff --git a/docs/features/text-labels.md b/docs/features/text-labels.md new file mode 100644 index 0000000..f3af762 --- /dev/null +++ b/docs/features/text-labels.md @@ -0,0 +1,107 @@ +--- +title: Text labels +uid: text-labels +--- + +# Text labels + +A text label is a piece of text rendered in the 3D world at a fixed position or attached to a moving entity (player or vehicle). They are commonly used for player name tags, item descriptions, signs, and floating labels above objectives. SampSharp distinguishes between **global** labels visible to every player () and **per-player** labels visible only to one player (). + +> [!NOTE] +> 3D text labels are different from . Text draws are 2D HUD overlays drawn on the screen; text labels are 3D objects positioned in the world. + +## Creating a text label + +Use for a label visible to every player: + +```csharp +[Event] +public void OnGameModeInit(IWorldService worldService) +{ + var label = worldService.CreateTextLabel( + text: "General Store", + color: new Color(255, 255, 255, 255), + position: new Vector3(1352, -1758, 16), + drawDistance: 20f, + virtualWorld: 0, + testLos: true); +} +``` + +Parameters worth knowing: + +- **`drawDistance`** — how close the player must be (in world units) before the label appears. Labels with very large draw distances cost more to render and clutter the view. +- **`testLos`** (line-of-sight) — when `true`, the label is hidden when there's geometry between the player and the label (walls, buildings). Set to `false` to make the label visible through walls. +- **`virtualWorld`** — use `0` for the default world. A value of `-1` hides the label entirely. +- **Multi-line text** — embed `\n` in the text to break lines, and use SA-MP color embedding (`{RRGGBB}`) inside the text to colour individual words. + +```csharp +worldService.CreateTextLabel( + text: "Ammu-Nation\n{FFFF00}Press F to enter", + color: new Color(255, 255, 255, 255), + position: new Vector3(1352, -1758, 16), + drawDistance: 20f); +``` + +## Player text labels + +A is created the same way, but only the owner sees it. Per-player labels are useful for personalized hints, quest markers, or floating debug info that shouldn't be visible to everyone: + +```csharp +[Event] +public void OnPlayerSpawn(Player player, IWorldService worldService) +{ + worldService.CreatePlayerTextLabel( + player: player, + text: "Welcome back!", + color: new Color(0, 255, 0, 255), + position: player.Position + new Vector3(0, 0, 2), + drawDistance: 10f); +} +``` + +A `PlayerTextLabel` is **not** automatically destroyed when the owner disconnects — pass `parent: player` if you want it to disappear with the player: + +```csharp +worldService.CreatePlayerTextLabel(player, "Hi", Color.White, pos, 10f, parent: player); +``` + +## Updating text and color + +For a global `TextLabel`, the `Text` and `Color` properties are writable, and `SetColorAndText` updates both in one call: + +```csharp +label.Text = "Store closed"; +label.Color = new Color(255, 0, 0, 255); + +// Or both at once +label.SetColorAndText(new Color(0, 255, 0, 255), "Store open"); +``` + +A `PlayerTextLabel` exposes `Text` and `Color` only as read-only properties — use `SetColorAndText` to change them: + +```csharp +playerLabel.SetColorAndText(new Color(0, 255, 0, 255), "Quest complete!"); +``` + +## Attaching to a player or vehicle + +A text label can follow a moving entity. The label is rendered at an offset relative to the attached entity, which is useful for player name tags, vehicle owner labels, or floating indicators above moving NPCs: + +```csharp +// Attach above a player's head +label.Attach(player, offset: new Vector3(0, 0, 1f)); + +// Attach to a vehicle (the offset is relative to the vehicle origin) +label.Attach(vehicle, offset: new Vector3(0, 0, 2f)); + +// Detach and place back in the world at an absolute position +label.DetachFromPlayer(new Vector3(1352, -1758, 16)); +label.DetachFromVehicle(new Vector3(1352, -1758, 16)); +``` + +`AttachedEntity`, `AttachedPlayer`, and `AttachedVehicle` let you inspect the current attachment. Setting a new attachment replaces the previous one. + +## Lifetime + +A `TextLabel` or `PlayerTextLabel` is destroyed when you call `Destroy()` on it, when its parent entity is destroyed, or when the server shuts down. A `PlayerTextLabel` is **not** implicitly tied to its owner's lifetime — see [Player text labels](#player-text-labels) 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 (label)` before use. See [Component liveness](xref:entities-components#component-liveness) for the full explanation. diff --git a/docs/features/vehicles.md b/docs/features/vehicles.md index cb66f4a..7df47ab 100644 --- a/docs/features/vehicles.md +++ b/docs/features/vehicles.md @@ -20,14 +20,35 @@ public void OnGameModeInit(IWorldService worldService) VehicleModelType.Infernus, new Vector3(1500, -1500, 14), // position 90f, // rotation (degrees) - color1: 1, // primary color - color2: 1 // secondary color + color1: -1, // -1 = random primary color + color2: -1, // -1 = random secondary color + respawnDelay: 60 // seconds without a driver before respawn; -1 disables respawn ); } ``` +A few parameter notes worth knowing: + +- Pass `-1` for `color1` / `color2` to get a random color. +- `respawnDelay` is in seconds; pass `-1` to disable automatic respawn entirely. +- `addSiren` only has an effect on vehicle models that already have a horn. + See for all available parameters. +### Static vehicles + + creates a vehicle with pre-loaded models, intended for permanent spawn points configured during `OnGameModeInit`. Beyond efficiency, it is also the only way to create train models (537 and 538) — regular `CreateVehicle` cannot spawn trains. + +```csharp +worldService.CreateStaticVehicle( + VehicleModelType.FreightTrain, // model 537 + new Vector3(1750, -1950, 14), + 0f, + color1: -1, color2: -1); +``` + +For everything else, use `CreateVehicle` so you can also create vehicles outside of init (mission spawns, garage purchases, etc.). + ## Handling Vehicle Events You can respond to vehicle-related events such as when a vehicle spawns, a player enters or exits a vehicle, and more. Here are some common event handlers: @@ -82,3 +103,7 @@ if (vehicle.HasTrailer) ``` See for all available properties and methods. + +## Lifetime + +A `Vehicle` component is destroyed when you call `Destroy()` on it, when its parent entity is destroyed, or when the server shuts down. As with any component, holding the reference across an `await` or timer callback can yield a destroyed instance — guard with `if (vehicle)` before use. See [Component liveness](xref:entities-components#component-liveness) for the full explanation. diff --git a/docs/toc.yml b/docs/toc.yml index 938836f..f4442e9 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -6,11 +6,17 @@ items: - href: core-concepts/entities-components.md - href: core-concepts/systems.md - href: core-concepts/events.md +- href: core-concepts/startup.md - name: Features - href: features/command-system.md - href: features/dialog-menus.md +- href: features/gang-zones.md +- href: features/npcs.md - href: features/objects.md +- href: features/pickups.md - href: features/players.md +- href: features/text-draws.md +- href: features/text-labels.md - href: features/timers.md - href: features/vehicles.md - name: Reference