Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs
2025-06-17 16:38:11 +03:00

457 lines
21 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable
{
private CoroutineHandle logPropertyChangeCoroutine;
public Inventory PreviousParentInventory;
public override Sprite Sprite
{
get { return base.Prefab?.Sprite; }
}
private readonly Dictionary<Client, CampaignMode.InteractionType> campaignInteractionTypePerClient = new Dictionary<Client, CampaignMode.InteractionType>();
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients)
{
if (Removed) { return; }
if (targetClients == null || targetClients.None())
{
campaignInteractionTypePerClient.Clear();
}
else
{
foreach (Client client in targetClients)
{
campaignInteractionTypePerClient[client] = interactionType;
}
}
GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData(targetClients));
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
Exception error(string reason)
{
string errorMsg = $"Failed to write a network event for the item \"{Name}\" - {reason}";
GameAnalyticsManager.AddErrorEventOnce($"Item.ServerWrite:{Name}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return new Exception(errorMsg);
}
if (extraData is null) { throw error("event data was null"); }
if (extraData is not IEventData itemEventData) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); }
msg.WriteRangedInteger((int)itemEventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue);
switch (itemEventData)
{
case ComponentStateEventData componentStateEventData:
int componentIndex = components.IndexOf(componentStateEventData.Component);
if (componentIndex < 0)
{
throw error($"component index out of range ({componentIndex})");
}
if (components[componentIndex] is not IServerSerializable serializableComponent)
{
throw error($"component \"{components[componentIndex]}\" is not server serializable");
}
msg.WriteRangedInteger(componentIndex, 0, components.Count - 1);
serializableComponent.ServerEventWrite(msg, c, extraData);
break;
case InventoryStateEventData inventoryStateEventData:
int containerIndex = components.IndexOf(inventoryStateEventData.Component);
if (containerIndex < 0)
{
throw error($"container index out of range ({containerIndex})");
}
if (components[containerIndex] is not ItemContainer itemContainer)
{
throw error("component \"" + components[containerIndex] + "\" is not server serializable");
}
msg.WriteRangedInteger(containerIndex, 0, components.Count - 1);
msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
itemContainer.Inventory.ServerEventWrite(msg, c, inventoryStateEventData);
break;
case ItemStatusEventData statusEvent:
msg.WriteBoolean(statusEvent.LoadingRound);
msg.WriteSingle(condition);
break;
case AssignCampaignInteractionEventData campaignInteractionData:
bool isVisibleToClient =
campaignInteractionData.TargetClients == null ||
campaignInteractionData.TargetClients.IsEmpty ||
campaignInteractionData.TargetClients.Contains(c);
msg.WriteBoolean(isVisibleToClient);
if (isVisibleToClient)
{
msg.WriteByte((byte)CampaignInteractionType);
}
break;
case ApplyStatusEffectEventData applyStatusEffectEventData:
{
ActionType actionType = applyStatusEffectEventData.ActionType;
ItemComponent targetComponent = applyStatusEffectEventData.TargetItemComponent;
Limb targetLimb = applyStatusEffectEventData.TargetLimb;
Vector2? worldPosition = applyStatusEffectEventData.WorldPosition;
Character targetCharacter = applyStatusEffectEventData.TargetCharacter;
if (targetCharacter != null && targetCharacter.Removed) { targetCharacter = null; }
byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255;
msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1);
msg.WriteByte((byte)(targetComponent == null ? 255 : components.IndexOf(targetComponent)));
msg.WriteUInt16(applyStatusEffectEventData.TargetCharacter?.ID ?? (ushort)0);
msg.WriteByte(targetLimbIndex);
msg.WriteUInt16(applyStatusEffectEventData.UseTarget?.ID ?? (ushort)0);
msg.WriteBoolean(worldPosition.HasValue);
if (worldPosition.HasValue)
{
msg.WriteSingle(worldPosition.Value.X);
msg.WriteSingle(worldPosition.Value.Y);
}
}
break;
case ChangePropertyEventData changePropertyEventData:
try
{
WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: !GameMain.NetworkMember.IsServer);
}
catch (Exception e)
{
throw new Exception(
$"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})");
}
break;
case SetItemStatEventData setItemStatEventData:
msg.WriteByte((byte)setItemStatEventData.Stats.Count);
foreach (var (key, value) in setItemStatEventData.Stats)
{
msg.WriteNetSerializableStruct(key);
msg.WriteSingle(value);
}
break;
case UpgradeEventData upgradeEventData:
var upgrade = upgradeEventData.Upgrade;
var upgradeTargets = upgrade.TargetComponents;
msg.WriteIdentifier(upgrade.Identifier);
msg.WriteByte((byte)upgrade.Level);
msg.WriteByte((byte)upgradeTargets.Count);
foreach (var (_, value) in upgrade.TargetComponents)
{
msg.WriteByte((byte)value.Length);
foreach (var propertyReference in value)
{
object originalValue = propertyReference.OriginalValue;
msg.WriteSingle((float)(originalValue ?? -1));
}
}
break;
case DroppedStackEventData droppedStackEventData:
msg.WriteRangedInteger(droppedStackEventData.Items.Length, 0, Inventory.MaxPossibleStackSize);
foreach (Item droppedItem in droppedStackEventData.Items)
{
msg.WriteUInt16(droppedItem.ID);
}
break;
case SetHighlightEventData highlightEventData:
bool isTargetedForClient =
highlightEventData.TargetClients.IsEmpty ||
highlightEventData.TargetClients.Contains(c);
msg.WriteBoolean(isTargetedForClient);
if (isTargetedForClient)
{
msg.WriteBoolean(highlightEventData.Highlighted);
if (highlightEventData.Highlighted)
{
msg.WriteColorR8G8B8A8(highlightEventData.Color);
}
}
break;
case SwapItemEventData swapItemEventData:
msg.WriteUInt16(swapItemEventData.NewId);
msg.WriteUInt32(swapItemEventData.NewItem.UintIdentifier);
break;
default:
throw error($"Unsupported event type {itemEventData.GetType().Name}");
}
}
public void ServerEventRead(IReadMessage msg, Client c)
{
EventType eventType =
(EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue);
c.KickAFKTimer = 0.0f;
switch (eventType)
{
case EventType.ComponentState:
int componentIndex = msg.ReadRangedInteger(0, components.Count - 1);
(components[componentIndex] as IClientSerializable).ServerEventRead(msg, c);
break;
case EventType.InventoryState:
int containerIndex = msg.ReadRangedInteger(0, components.Count - 1);
(components[containerIndex] as ItemContainer).Inventory.ServerEventRead(msg, c);
break;
case EventType.Treatment:
if (c.Character == null || !c.Character.CanInteractWith(this)) { return; }
UInt16 characterID = msg.ReadUInt16();
byte limbIndex = msg.ReadByte();
if (HealingCooldown.IsOnCooldown(c)) { return; }
if (FindEntityByID(characterID) is not Character targetCharacter) { break; }
if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) { break; }
HealingCooldown.SetCooldown(c);
Limb targetLimb = limbIndex < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[limbIndex] : null;
if (ContainedItems == null || ContainedItems.All(static i => i == null))
{
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} used item {Name}", ServerLog.MessageType.ItemInteraction);
}
else
{
GameServer.Log(
$"{GameServer.CharacterLogName(c.Character)} used item {Name} (contained items: {string.Join(", ", ContainedItems.Select(i => i.Name))})",
ServerLog.MessageType.ItemInteraction);
}
ApplyTreatment(c.Character, targetCharacter, targetLimb);
break;
case EventType.ChangeProperty:
ReadPropertyChange(msg, inGameEditableOnly: GameMain.NetworkMember.IsServer, sender: c);
break;
case EventType.Combine:
UInt16 combineTargetID = msg.ReadUInt16();
Item combineTarget = FindEntityByID(combineTargetID) as Item;
if (combineTarget == null || !c.Character.CanInteractWith(this) || !c.Character.CanInteractWith(combineTarget))
{
return;
}
Combine(combineTarget, c.Character);
break;
}
}
public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex, int originalSlotIndex)
{
if (GameMain.Server == null) { return; }
msg.WriteString(Prefab.OriginalName);
msg.WriteIdentifier(Prefab.Identifier);
msg.WriteBoolean(Description != base.Prefab.Description);
if (Description != base.Prefab.Description)
{
msg.WriteString(Description);
}
msg.WriteUInt16(entityID);
if (ParentInventory == null || ParentInventory.Owner == null || originalInventoryID == 0)
{
msg.WriteUInt16((ushort)0);
msg.WriteSingle(Position.X);
msg.WriteSingle(Position.Y);
msg.WriteRangedSingle(body == null ? 0.0f : MathUtils.WrapAngleTwoPi(body.Rotation), 0.0f, MathHelper.TwoPi, 8);
msg.WriteUInt16(Submarine != null ? Submarine.ID : (ushort)0);
}
else
{
msg.WriteUInt16(originalInventoryID);
msg.WriteByte(originalItemContainerIndex);
msg.WriteByte(originalSlotIndex < 0 ? (byte)255 : (byte)originalSlotIndex);
}
msg.WriteBoolean(OnInsertedEffectsAppliedOnPreviousRound);
msg.WriteByte(body == null ? (byte)0 : (byte)body.BodyType);
msg.WriteBoolean(SpawnedInCurrentOutpost);
msg.WriteBoolean(AllowStealing);
msg.WriteRangedInteger(Quality, 0, Items.Components.Quality.MaxQuality);
byte teamID = 0;
IdCard idCardComponent = null;
foreach (WifiComponent wifiComponent in GetComponents<WifiComponent>())
{
teamID = (byte)wifiComponent.TeamID;
break;
}
if (teamID == 0)
{
foreach (IdCard idCard in GetComponents<IdCard>())
{
teamID = (byte)idCard.TeamID;
idCardComponent = idCard;
break;
}
}
msg.WriteByte(teamID);
bool hasIdCard = idCardComponent != null;
msg.WriteBoolean(hasIdCard);
if (hasIdCard)
{
msg.WriteInt32(idCardComponent.SubmarineSpecificID);
msg.WriteString(idCardComponent.OwnerName);
msg.WriteString(idCardComponent.OwnerTags);
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex + 1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerHairIndex + 1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex + 1));
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex + 1));
msg.WriteColorR8G8B8(idCardComponent.OwnerHairColor);
msg.WriteColorR8G8B8(idCardComponent.OwnerFacialHairColor);
msg.WriteColorR8G8B8(idCardComponent.OwnerSkinColor);
msg.WriteIdentifier(idCardComponent.OwnerJobId);
msg.WriteByte((byte)idCardComponent.OwnerSheetIndex.X);
msg.WriteByte((byte)idCardComponent.OwnerSheetIndex.Y);
}
bool tagsChanged = tags.Count != base.Prefab.Tags.Count || !tags.All(t => base.Prefab.Tags.Contains(t));
msg.WriteBoolean(tagsChanged);
if (tagsChanged)
{
IEnumerable<Identifier> splitTags = Tags.ToIdentifiers();
msg.WriteString(string.Join(',', splitTags.Where(t => !base.Prefab.Tags.Contains(t))));
msg.WriteString(string.Join(',', base.Prefab.Tags.Where(t => !splitTags.Contains(t))));
}
var nameTag = GetComponent<NameTag>();
msg.WriteBoolean(nameTag != null);
if (nameTag != null)
{
msg.WriteString(nameTag.WrittenName ?? "");
}
}
partial void UpdateNetPosition(float deltaTime)
{
if (parentInventory != null || body == null || !body.Enabled || Removed)
{
PositionUpdateInterval = float.PositiveInfinity;
return;
}
//gradually increase the interval of position updates
PositionUpdateInterval += deltaTime;
float maxInterval = 30.0f;
float velSqr = body.LinearVelocity.LengthSquared();
if (velSqr > 10.0f * 10.0f)
{
//over 10 m/s (projectile, thrown item or similar) -> send updates very frequently
maxInterval = 0.1f;
}
else if (velSqr > 1.0f)
{
//over 1 m/s
maxInterval = 0.25f;
}
else if (velSqr > 0.05f * 0.05f)
{
//over 0.05 m/s
maxInterval = 1.0f;
}
PositionUpdateInterval = Math.Min(PositionUpdateInterval, maxInterval);
}
public float GetPositionUpdateInterval(Client recipient)
{
if (PositionUpdateInterval == float.PositiveInfinity || body == null || parentInventory != null)
{
return float.PositiveInfinity;
}
if (recipient.Character == null || recipient.Character.IsDead)
{
//less frequent updates for clients who aren't controlling a character (max 2 updates/sec)
return Math.Max(PositionUpdateInterval, 0.5f);
}
else
{
float distSqr = Vector2.DistanceSquared(recipient.Character.WorldPosition, WorldPosition);
if (distSqr > 20000.0f * 20000.0f)
{
//don't send position updates at all if >20 000 units away
return float.PositiveInfinity;
}
else if (distSqr > 10000.0f * 10000.0f)
{
//drop the update rate to 10% if too far to see the item
return PositionUpdateInterval * 10;
}
else if (distSqr > 1000.0f * 1000.0f)
{
//halve the update rate if the client is far away (but still close enough to possibly see the item)
return PositionUpdateInterval * 2;
}
return PositionUpdateInterval;
}
}
public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c)
{
body.ServerWrite(tempBuffer);
}
public void CreateServerEvent<T>(T ic) where T : ItemComponent, IServerSerializable
=> CreateServerEvent(ic, ic.ServerGetEventData());
public void CreateServerEvent<T>(T ic, ItemComponent.IEventData extraData) where T : ItemComponent, IServerSerializable
{
if (GameMain.Server == null) { return; }
if (!ItemList.Contains(this))
{
string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
}
#warning TODO: this should throw an exception
if (!components.Contains(ic)) { return; }
var eventData = new ComponentStateEventData(ic, extraData);
if (!ic.ValidateEventData(eventData))
{
string errorMsg =
$"Server-side component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false. " +
$"Data: {extraData?.GetType().ToString() ?? "null"}";
GameAnalyticsManager.AddErrorEventOnce($"Item.CreateServerEvent:ValidateEventData:{Prefab.Identifier}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
GameMain.Server.CreateEntityEvent(this, eventData);
}
#if DEBUG
public void TryCreateServerEventSpam()
{
if (GameMain.Server == null) { return; }
foreach (ItemComponent ic in components)
{
if (ic is not IServerSerializable) { continue; }
var eventData = ic.ServerGetEventData();
if (eventData == null) { continue; }
var componentData = new ComponentStateEventData(ic, eventData);
if (!ic.ValidateEventData(componentData)) { continue; }
GameMain.Server.CreateEntityEvent(this, componentData);
}
}
#endif
}
}