PlayerInventoryInput.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Owner-side inventory switching input for any player using BaseInventoryComponent.
///
/// This component deliberately knows nothing about OneMoreRoundWeapon or OMR game
/// modes. It translates configured input actions into inventory slot/cycle requests;
/// BaseInventoryComponent remains host authoritative and routes client switches to
/// the host.
/// </summary>
public sealed class PlayerInventoryInput : Component
{
[Property, Group( "References" )]
public BaseInventoryComponent Inventory { get; set; }
/// <summary>
/// Optional OMR control gate. When present, weapon switching is disabled whenever
/// movement/gameplay controls are disabled (menus, countdown locks, etc.). Leave
/// this null in another project to allow switching whenever this component is enabled.
/// </summary>
[Property, Group( "References" )]
public PlayerMovementInput ControlsGate { get; set; }
[Property, Group( "Input" )]
public bool DirectSlotsEnabled { get; set; } = true;
[Property, Group( "Input" )]
public bool CyclingEnabled { get; set; } = true;
[Property, Group( "Input" )]
public string SlotActionPrefix { get; set; } = "Slot";
[Property, Group( "Input" )]
public string PreviousSlotAction { get; set; } = "SlotPrev";
[Property, Group( "Input" )]
public string NextSlotAction { get; set; } = "SlotNext";
[Property, Group( "Behaviour" )]
public bool SkipUnswitchableItems { get; set; } = true;
[Property, Group( "Debug" )]
public bool LogSwitches { get; set; } = false;
protected override void OnStart()
{
Inventory ??=
Components.Get<BaseInventoryComponent>();
ControlsGate ??=
Components.Get<PlayerMovementInput>();
if ( Inventory is null )
{
Log.Warning(
"PlayerInventoryInput could not find BaseInventoryComponent."
);
}
}
protected override void OnUpdate()
{
if ( IsProxy )
return;
if ( Components.Get<PlayerState>()?.IsBot == true )
return;
if ( Inventory is null )
return;
if (
ControlsGate is not null &&
!ControlsGate.ControlsEnabled
)
return;
// Direct slot selection wins when multiple inventory actions happen to be
// pressed in the same frame. This avoids issuing two host-routed switches.
if (
DirectSlotsEnabled &&
TryHandleDirectSlotInput()
)
return;
if ( !CyclingEnabled )
return;
if (
!string.IsNullOrWhiteSpace( PreviousSlotAction ) &&
Input.Pressed( PreviousSlotAction )
)
{
CycleInventory( -1 );
return;
}
if (
!string.IsNullOrWhiteSpace( NextSlotAction ) &&
Input.Pressed( NextSlotAction )
)
{
CycleInventory( 1 );
}
}
private bool TryHandleDirectSlotInput()
{
if ( string.IsNullOrWhiteSpace( SlotActionPrefix ) )
return false;
// Conventional FPS mapping:
// Slot1 -> inventory slot 0 ... Slot9 -> slot 8, Slot0 -> slot 9.
for ( int displayedSlot = 1; displayedSlot <= 9; displayedSlot++ )
{
string action =
$"{SlotActionPrefix}{displayedSlot}";
if ( !Input.Pressed( action ) )
continue;
SwitchToSlot(
displayedSlot - 1
);
return true;
}
string zeroAction =
$"{SlotActionPrefix}0";
if ( Input.Pressed( zeroAction ) )
{
SwitchToSlot( 9 );
return true;
}
return false;
}
private void SwitchToSlot(
int slot
)
{
if (
slot < 0 ||
slot >= Inventory.MaxSlots
)
return;
BaseInventoryItem target =
Inventory.GetSlot( slot );
TrySwitchTo(
target,
$"slot {slot + 1}"
);
}
private void CycleInventory(
int direction
)
{
if ( direction == 0 )
return;
List<BaseInventoryItem> candidates =
Inventory.Items
.Where( IsCycleCandidate )
.ToList();
if ( candidates.Count <= 0 )
return;
BaseInventoryItem active =
Inventory.ActiveItem;
int currentIndex =
candidates.IndexOf( active );
if ( currentIndex < 0 )
{
currentIndex =
direction > 0
? -1
: 0;
}
for ( int attempt = 0; attempt < candidates.Count; attempt++ )
{
currentIndex =
WrapIndex(
currentIndex + Math.Sign( direction ),
candidates.Count
);
BaseInventoryItem target =
candidates[currentIndex];
if ( target == active )
continue;
if (
TrySwitchTo(
target,
direction > 0
? "next"
: "previous"
)
)
return;
}
}
private bool IsCycleCandidate(
BaseInventoryItem item
)
{
if ( item is null )
return false;
if ( item == Inventory.ActiveItem )
return true;
if ( !SkipUnswitchableItems )
return true;
return item.CanSwitchTo();
}
private bool TrySwitchTo(
BaseInventoryItem target,
string reason
)
{
if ( target is null )
return false;
if ( target == Inventory.ActiveItem )
return false;
if (
SkipUnswitchableItems &&
!target.CanSwitchTo()
)
return false;
if ( LogSwitches )
{
string from =
Inventory.ActiveItem?.DisplayName ??
"<none>";
string to =
target.DisplayName ??
target.GameObject?.Name ??
"<unnamed>";
Log.Info(
$"[INVENTORY INPUT] {reason} | {from} -> {to} | Slot:{target.Slot}"
);
}
// BaseInventoryComponent.Switch is host authoritative and client calls are
// routed through the host. false keeps re-selecting the active slot a no-op.
Inventory.Switch(
target,
false
);
return true;
}
private static int WrapIndex(
int index,
int count
)
{
if ( count <= 0 )
return 0;
index %= count;
if ( index < 0 )
index += count;
return index;
}
}