Host-side RPC handlers for the roleplay inventory. Validates the caller, then handles use, move/merge/swap, drop, give and pickup, reading item and quantity from the host's own inventory copy.
namespace HorizonRP;
/// <summary>
/// Ce que l'inventaire (touche I, cases rapides 1 à 5) demande à l'hôte. L'hôte
/// relit tout depuis son propre inventaire : le client ne désigne qu'un numéro
/// de case, jamais un objet ni une quantité qu'il n'aurait pas.
/// </summary>
public static class InventoryActions
{
/// <summary>Distance pour donner un objet ou ramasser au sol (~2,5 m).</summary>
public const float Reach = 100f;
static RoleplayPlayer Caller() =>
RoleplayPlayer.All.FirstOrDefault( x => Rpc.Caller is not null && x.Network.OwnerId == Rpc.Caller.Id && x.ProfileLoaded );
/// <summary>Menotté (Police/) : les mains dans le dos, on ne touche plus à ses poches.</summary>
static RoleplayPlayer FreeCaller()
{
var player = Caller();
if ( player is { Cuffed: true } )
{
player.Notify( "Vous êtes menotté.", "erreur" );
return null;
}
return player;
}
/// <summary>Une arme de service (Police/) ne quitte pas l'agent : ni jetée, ni donnée.</summary>
static bool RefuseService( RoleplayPlayer player, InventorySlot slot, string what )
{
if ( !PoliceService.IsServiceSlot( slot ) )
return false;
player.Notify( $"Arme de service : elle ne se {what} pas. Rendez-la au vestiaire.", "erreur" );
return true;
}
[Rpc.Host]
public static void RequestUse( int slotIndex )
{
var player = FreeCaller();
var slot = player?.SlotAt( slotIndex );
var item = slot?.Definition;
if ( item is null )
return;
if ( player.GameObject.GetComponentInParent<Vehicle>() is { } car && car.Driver == player )
{
player.Notify( "Pas d'inventaire en conduisant.", "info" );
return;
}
// Une clé de véhicule se prend en main ou se range.
if ( VehicleKeys.PlateOf( item.Id ) is not null )
{
VehicleKeys.ToggleHold( player, slot );
return;
}
// Un prop se prend en main (carton à deux mains, ou le vrai digicode, le vrai terminal) ;
// en main, le clic gauche le pose (VehicleKeySystem.LocalInput).
if ( PropItems.EntryOf( item.Id ) > 0 )
{
VehicleKeys.ToggleHold( player, slot );
return;
}
// Une arme se sort ou se range ; elle ne se « consomme » pas.
if ( WeaponItems.WeaponOf( item.Id ) is not null )
{
WeaponSystem.Current?.ToggleWeapon( player, slot );
return;
}
if ( !item.Usable )
{
player.Notify( $"{item.Name} ne s'utilise pas.", "info" );
return;
}
if ( item.Hunger > 0 && player.Hunger >= 99 && item.Health <= 0 )
{
player.Notify( "Vous n'avez plus faim.", "info" );
return;
}
if ( item.Health > 0 && player.Health >= 100 && item.Hunger <= 0 )
{
player.Notify( "Vous êtes en pleine forme.", "info" );
return;
}
player.Hunger = MathF.Min( 100, player.Hunger + item.Hunger );
player.Health = MathF.Min( 100, player.Health + item.Health );
player.RemoveFrom( slot, 1 );
player.Notify( item.UseMessage ?? $"{item.Name} utilisé.", "info" );
}
/// <summary>Déplace une case : vers une case vide, sur la même pile (fusion) ou sur un autre objet (échange).</summary>
[Rpc.Host]
public static void RequestMove( int from, int to )
{
var player = FreeCaller();
if ( player is null || from == to || to < 0 || to >= InventoryRules.Slots )
return;
var source = player.SlotAt( from );
if ( source is null )
return;
var target = player.SlotAt( to );
if ( target is null )
{
source.Slot = to;
}
else if ( target.Item == source.Item )
{
var max = source.Definition.MaxStack;
var moved = Math.Min( source.Count, max - target.Count );
target.Count += moved;
source.Count -= moved;
if ( source.Count <= 0 )
player.Inventory.Remove( source );
}
else
{
(source.Slot, target.Slot) = (target.Slot, source.Slot);
}
player.PublishInventory();
}
[Rpc.Host]
public static void RequestDrop( int slotIndex, int count )
{
var player = FreeCaller();
var slot = player?.SlotAt( slotIndex );
if ( slot is null || RefuseService( player, slot, "jette" ) )
return;
count = Math.Clamp( count, 1, slot.Count );
var item = slot.Definition;
var data = slot.Data;
WeaponSystem.Current?.HolsterIfActive( player, slot );
player.RemoveFrom( slot, count );
// Devant les pieds, posé au sol.
var start = player.WorldPosition + player.WorldRotation.Forward.WithZ( 0 ).Normal * 40 + Vector3.Up * 40;
var ground = player.Scene.Trace.Ray( start, start + Vector3.Down * 200 ).IgnoreGameObjectHierarchy( player.GameObject ).Run();
DroppedItem.Spawn( item, count, ground.Hit ? ground.EndPosition : player.WorldPosition, data );
player.Notify( $"{item.Name} ×{count} jeté au sol.", "info" );
}
[Rpc.Host]
public static void RequestGive( int slotIndex, int count, ulong targetSteamId )
{
var player = FreeCaller();
var slot = player?.SlotAt( slotIndex );
var target = RoleplayPlayer.All.FirstOrDefault( x => x.SteamId == targetSteamId && x.ProfileLoaded );
if ( slot is null || target is null || target == player || RefuseService( player, slot, "donne" ) )
return;
if ( target.WorldPosition.Distance( player.WorldPosition ) > Reach * 1.5f )
{
player.Notify( $"Approchez-vous de {target.RpName}.", "erreur" );
return;
}
var item = slot.Definition;
count = Math.Clamp( count, 1, slot.Count );
WeaponSystem.Current?.HolsterIfActive( player, slot );
var given = target.AddItem( item, count, slot.Data );
if ( given == 0 )
{
player.Notify( $"{target.RpName} n'a plus de place.", "erreur" );
return;
}
player.RemoveFrom( slot, given );
player.Notify( $"Vous donnez {item.Name} ×{given} à {target.RpName}.", "info" );
target.Notify( $"{player.RpName} vous donne {item.Name} ×{given}.", "info" );
HorizonGame.Current?.AddLog( "objet_donne", player.SteamId, $"{item.Name} ×{given} à {target.RpName} ({target.SteamId})" );
}
[Rpc.Host]
public static void RequestPickup( Guid droppedId )
{
var player = FreeCaller();
var dropped = DroppedItem.All.FirstOrDefault( x => x.GameObject.Id == droppedId );
if ( player is null || dropped is null || dropped.Definition is null )
return;
if ( dropped.WorldPosition.Distance( player.WorldPosition ) > Reach * 2 )
{
player.Notify( "Trop loin.", "erreur" );
return;
}
var taken = player.AddItem( dropped.Definition, dropped.Count, dropped.Data );
if ( taken == 0 )
{
player.Notify( "Inventaire plein ou trop lourd.", "erreur" );
return;
}
player.Notify( $"Ramassé : {dropped.Definition.Name} ×{taken}", "info" );
dropped.Count -= taken;
if ( dropped.Count <= 0 )
dropped.GameObject.Destroy();
}
}