* Borg type switching. This allows borgs (new spawn or constructed) to select their chassis type on creation, like in SS13. This removes the need for the many different chassis types, and means round-start borgs can actually play the game immediately instead of waiting for science to unlock everything. New borgs have an additional action that allows them to select their type. This opens a nice window with basic information about the borgs and a select button. Once a type has been selected it is permanent for that borg chassis. These borg types also immediately start the borg with specific modules, so they do not need to be printed. Additional modules can still be inserted for upgrades, though this is now less critical. The built-in modules cannot be removed, but are shown in the UI. The modules that each borg type starts with: * Generic: tools * Engineering: advanced tools, construction, RCD, cable * Salvage: Grappling gun, appraisal, mining * Janitor: cleaning, light replacer * Medical: treatment * Service: music, service, clowning Specialized borgs have 3 additional module slots available on top of the ones listed above, generic borgs have 5. Borg types are specified in a new BorgTypePrototype. These prototypes specify all information about the borg type. It is assigned to the borg entity through a mix of client side, server, and shared code. Some of the involved components were made networked, others are just ensured they're set on both sides of the wire. The most gnarly change is the inventory template prototype, which needs to change purely to modify the borg hat offset. I managed to bodge this in with an API that *probably* won't explode for specifically for this use case, but it's still not the most clean of API designs. Parts for specific borg chassis have been removed (so much deleted YAML) and specialized borg modules that are in the base set of a type have been removed from the exosuit fab as there's no point to printing those. The ability to "downgrade" a borg so it can select a new chassis, like in SS13, is something that would be nice, but was not high enough priority for me to block the feature on. I did keep it in mind with some of the code, so it may be possible in the future. There is no fancy animation when selecting borg types like in SS13, because I didn't think it was high priority, and it would add a lot of complex code. * Fix sandbox failure due to collection expression. * Module tweak Fix salvage borg modules still having research/lathe recipes Engie borg has regular tool module, not advanced. * Fix inventory system breakage * Fix migrations Some things were missing * Guidebook rewordings & review * MinWidth on confirm selection button
306 lines
11 KiB
C#
306 lines
11 KiB
C#
using Content.Client.Clothing;
|
|
using Content.Client.Examine;
|
|
using Content.Client.Verbs.UI;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Inventory;
|
|
using Content.Shared.Inventory.Events;
|
|
using Content.Shared.Storage;
|
|
using JetBrains.Annotations;
|
|
using Robust.Client.Player;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Input.Binding;
|
|
using Robust.Shared.Player;
|
|
|
|
namespace Content.Client.Inventory
|
|
{
|
|
[UsedImplicitly]
|
|
public sealed class ClientInventorySystem : InventorySystem
|
|
{
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
|
|
|
[Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!;
|
|
[Dependency] private readonly ExamineSystem _examine = default!;
|
|
|
|
public Action<SlotData>? EntitySlotUpdate = null;
|
|
public Action<SlotData>? OnSlotAdded = null;
|
|
public Action<SlotData>? OnSlotRemoved = null;
|
|
public Action<EntityUid, InventorySlotsComponent>? OnLinkInventorySlots = null;
|
|
public Action? OnUnlinkInventory = null;
|
|
public Action<SlotSpriteUpdate>? OnSpriteUpdate = null;
|
|
|
|
private readonly Queue<(InventorySlotsComponent comp, EntityEventArgs args)> _equipEventsQueue = new();
|
|
|
|
public override void Initialize()
|
|
{
|
|
UpdatesOutsidePrediction = true;
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<InventorySlotsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
|
SubscribeLocalEvent<InventorySlotsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
|
|
|
SubscribeLocalEvent<InventoryComponent, ComponentShutdown>(OnShutdown);
|
|
|
|
SubscribeLocalEvent<InventorySlotsComponent, DidEquipEvent>((_, comp, args) =>
|
|
_equipEventsQueue.Enqueue((comp, args)));
|
|
SubscribeLocalEvent<InventorySlotsComponent, DidUnequipEvent>((_, comp, args) =>
|
|
_equipEventsQueue.Enqueue((comp, args)));
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
while (_equipEventsQueue.TryDequeue(out var tuple))
|
|
{
|
|
var (component, args) = tuple;
|
|
|
|
switch (args)
|
|
{
|
|
case DidEquipEvent equipped:
|
|
OnDidEquip(component, equipped);
|
|
break;
|
|
case DidUnequipEvent unequipped:
|
|
OnDidUnequip(component, unequipped);
|
|
break;
|
|
default:
|
|
throw new InvalidOperationException($"Received queued event of unknown type: {args.GetType()}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnDidUnequip(InventorySlotsComponent component, DidUnequipEvent args)
|
|
{
|
|
UpdateSlot(args.Equipee, component, args.Slot);
|
|
if (args.Equipee != _playerManager.LocalEntity)
|
|
return;
|
|
var update = new SlotSpriteUpdate(null, args.SlotGroup, args.Slot, false);
|
|
OnSpriteUpdate?.Invoke(update);
|
|
}
|
|
|
|
private void OnDidEquip(InventorySlotsComponent component, DidEquipEvent args)
|
|
{
|
|
UpdateSlot(args.Equipee, component, args.Slot);
|
|
if (args.Equipee != _playerManager.LocalEntity)
|
|
return;
|
|
var update = new SlotSpriteUpdate(args.Equipment, args.SlotGroup, args.Slot,
|
|
HasComp<StorageComponent>(args.Equipment));
|
|
OnSpriteUpdate?.Invoke(update);
|
|
}
|
|
|
|
private void OnShutdown(EntityUid uid, InventoryComponent component, ComponentShutdown args)
|
|
{
|
|
if (uid == _playerManager.LocalEntity)
|
|
OnUnlinkInventory?.Invoke();
|
|
}
|
|
|
|
private void OnPlayerDetached(EntityUid uid, InventorySlotsComponent component, LocalPlayerDetachedEvent args)
|
|
{
|
|
OnUnlinkInventory?.Invoke();
|
|
}
|
|
|
|
private void OnPlayerAttached(EntityUid uid, InventorySlotsComponent component, LocalPlayerAttachedEvent args)
|
|
{
|
|
if (TryGetSlots(uid, out var definitions))
|
|
{
|
|
foreach (var definition in definitions)
|
|
{
|
|
if (!TryGetSlotContainer(uid, definition.Name, out var container, out _))
|
|
continue;
|
|
|
|
if (!component.SlotData.TryGetValue(definition.Name, out var data))
|
|
{
|
|
data = new SlotData(definition);
|
|
component.SlotData[definition.Name] = data;
|
|
}
|
|
|
|
data.Container = container;
|
|
}
|
|
}
|
|
|
|
OnLinkInventorySlots?.Invoke(uid, component);
|
|
}
|
|
|
|
public override void Shutdown()
|
|
{
|
|
CommandBinds.Unregister<ClientInventorySystem>();
|
|
base.Shutdown();
|
|
}
|
|
|
|
protected override void OnInit(EntityUid uid, InventoryComponent component, ComponentInit args)
|
|
{
|
|
base.OnInit(uid, component, args);
|
|
_clothingVisualsSystem.InitClothing(uid, component);
|
|
|
|
if (!TryComp(uid, out InventorySlotsComponent? inventorySlots))
|
|
return;
|
|
|
|
foreach (var slot in component.Slots)
|
|
{
|
|
TryAddSlotDef(uid, inventorySlots, slot);
|
|
}
|
|
}
|
|
|
|
public void ReloadInventory(InventorySlotsComponent? component = null)
|
|
{
|
|
var player = _playerManager.LocalEntity;
|
|
if (player == null || !Resolve(player.Value, ref component, false))
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnUnlinkInventory?.Invoke();
|
|
OnLinkInventorySlots?.Invoke(player.Value, component);
|
|
}
|
|
|
|
public void SetSlotHighlight(EntityUid owner, InventorySlotsComponent component, string slotName, bool state)
|
|
{
|
|
var oldData = component.SlotData[slotName];
|
|
var newData = component.SlotData[slotName] = new SlotData(oldData, state);
|
|
if (owner == _playerManager.LocalEntity)
|
|
EntitySlotUpdate?.Invoke(newData);
|
|
}
|
|
|
|
public void UpdateSlot(EntityUid owner, InventorySlotsComponent component, string slotName,
|
|
bool? blocked = null, bool? highlight = null)
|
|
{
|
|
var oldData = component.SlotData[slotName];
|
|
var newHighlight = oldData.Highlighted;
|
|
var newBlocked = oldData.Blocked;
|
|
|
|
if (blocked != null)
|
|
newBlocked = blocked.Value;
|
|
|
|
if (highlight != null)
|
|
newHighlight = highlight.Value;
|
|
|
|
var newData = component.SlotData[slotName] =
|
|
new SlotData(component.SlotData[slotName], newHighlight, newBlocked);
|
|
if (owner == _playerManager.LocalEntity)
|
|
EntitySlotUpdate?.Invoke(newData);
|
|
}
|
|
|
|
public bool TryAddSlotDef(EntityUid owner, InventorySlotsComponent component, SlotDefinition newSlotDef)
|
|
{
|
|
SlotData newSlotData = newSlotDef; //convert to slotData
|
|
if (!component.SlotData.TryAdd(newSlotDef.Name, newSlotData))
|
|
return false;
|
|
|
|
if (owner == _playerManager.LocalEntity)
|
|
OnSlotAdded?.Invoke(newSlotData);
|
|
return true;
|
|
}
|
|
|
|
public void UIInventoryActivate(string slot)
|
|
{
|
|
EntityManager.RaisePredictiveEvent(new UseSlotNetworkMessage(slot));
|
|
}
|
|
|
|
public void UIInventoryStorageActivate(string slot)
|
|
{
|
|
EntityManager.RaisePredictiveEvent(new OpenSlotStorageNetworkMessage(slot));
|
|
}
|
|
|
|
public void UIInventoryExamine(string slot, EntityUid uid)
|
|
{
|
|
if (!TryGetSlotEntity(uid, slot, out var item))
|
|
return;
|
|
|
|
_examine.DoExamine(item.Value);
|
|
}
|
|
|
|
public void UIInventoryOpenContextMenu(string slot, EntityUid uid)
|
|
{
|
|
if (!TryGetSlotEntity(uid, slot, out var item))
|
|
return;
|
|
|
|
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(item.Value);
|
|
}
|
|
|
|
public void UIInventoryActivateItem(string slot, EntityUid uid)
|
|
{
|
|
if (!TryGetSlotEntity(uid, slot, out var item))
|
|
return;
|
|
|
|
EntityManager.RaisePredictiveEvent(
|
|
new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: false));
|
|
}
|
|
|
|
public void UIInventoryAltActivateItem(string slot, EntityUid uid)
|
|
{
|
|
if (!TryGetSlotEntity(uid, slot, out var item))
|
|
return;
|
|
|
|
EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: true));
|
|
}
|
|
|
|
protected override void UpdateInventoryTemplate(Entity<InventoryComponent> ent)
|
|
{
|
|
base.UpdateInventoryTemplate(ent);
|
|
|
|
if (TryComp(ent, out InventorySlotsComponent? inventorySlots))
|
|
{
|
|
foreach (var slot in ent.Comp.Slots)
|
|
{
|
|
if (inventorySlots.SlotData.TryGetValue(slot.Name, out var slotData))
|
|
slotData.SlotDef = slot;
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class SlotData
|
|
{
|
|
public SlotDefinition SlotDef;
|
|
public EntityUid? HeldEntity => Container?.ContainedEntity;
|
|
public bool Blocked;
|
|
public bool Highlighted;
|
|
|
|
[ViewVariables]
|
|
public ContainerSlot? Container;
|
|
public bool HasSlotGroup => SlotDef.SlotGroup != "Default";
|
|
public Vector2i ButtonOffset => SlotDef.UIWindowPosition;
|
|
public string SlotName => SlotDef.Name;
|
|
public bool ShowInWindow => SlotDef.ShowInWindow;
|
|
public string SlotGroup => SlotDef.SlotGroup;
|
|
public string SlotDisplayName => SlotDef.DisplayName;
|
|
public string TextureName => "Slots/" + SlotDef.TextureName;
|
|
public string FullTextureName => SlotDef.FullTextureName;
|
|
|
|
public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false,
|
|
bool blocked = false)
|
|
{
|
|
SlotDef = slotDef;
|
|
Highlighted = highlighted;
|
|
Blocked = blocked;
|
|
Container = container;
|
|
}
|
|
|
|
public SlotData(SlotData oldData, bool highlighted = false, bool blocked = false)
|
|
{
|
|
SlotDef = oldData.SlotDef;
|
|
Highlighted = highlighted;
|
|
Container = oldData.Container;
|
|
Blocked = blocked;
|
|
}
|
|
|
|
public static implicit operator SlotData(SlotDefinition s)
|
|
{
|
|
return new SlotData(s);
|
|
}
|
|
|
|
public static implicit operator SlotDefinition(SlotData s)
|
|
{
|
|
return s.SlotDef;
|
|
}
|
|
}
|
|
|
|
public readonly record struct SlotSpriteUpdate(
|
|
EntityUid? Entity,
|
|
string Group,
|
|
string Name,
|
|
bool ShowStorage
|
|
);
|
|
}
|
|
}
|