emag refactor (#15181)
* limitedcharges stuff from emag * changes except broken * fix * the * move recharging to server, emag namespace -> charges * the * use resolve * pro webedit gaming * the * the --------- Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
5
Content.Client/Charges/Systems/ChargesSystem.cs
Normal file
5
Content.Client/Charges/Systems/ChargesSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Charges.Systems;
|
||||
|
||||
namespace Content.Client.Charges.Systems;
|
||||
|
||||
public sealed class ChargesSystem : SharedChargesSystem { }
|
||||
25
Content.Server/Charges/Components/AutoRechargeComponent.cs
Normal file
25
Content.Server/Charges/Components/AutoRechargeComponent.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Content.Server.Charges.Systems;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Charges.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Something with limited charges that can be recharged automatically.
|
||||
/// Requires LimitedChargesComponent to function.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(ChargesSystem))]
|
||||
public sealed class AutoRechargeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The time it takes to regain a single charge
|
||||
/// </summary>
|
||||
[DataField("rechargeDuration"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
|
||||
|
||||
/// <summary>
|
||||
/// The time when the next charge will be added
|
||||
/// </summary>
|
||||
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextChargeTime = TimeSpan.MaxValue;
|
||||
}
|
||||
63
Content.Server/Charges/Systems/ChargesSystem.cs
Normal file
63
Content.Server/Charges/Systems/ChargesSystem.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Content.Server.Charges.Components;
|
||||
using Content.Shared.Charges.Components;
|
||||
using Content.Shared.Charges.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Charges.Systems;
|
||||
|
||||
public sealed class ChargesSystem : SharedChargesSystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AutoRechargeComponent, EntityUnpausedEvent>(OnUnpaused);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<LimitedChargesComponent, AutoRechargeComponent>();
|
||||
while (query.MoveNext(out var uid, out var charges, out var recharge))
|
||||
{
|
||||
if (charges.Charges == charges.MaxCharges || _timing.CurTime < recharge.NextChargeTime)
|
||||
continue;
|
||||
|
||||
AddCharges(uid, 1, charges);
|
||||
recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnpaused(EntityUid uid, AutoRechargeComponent comp, ref EntityUnpausedEvent args)
|
||||
{
|
||||
comp.NextChargeTime += args.PausedTime;
|
||||
}
|
||||
|
||||
protected override void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
|
||||
{
|
||||
base.OnExamine(uid, comp, args);
|
||||
|
||||
// only show the recharging info if it's not full
|
||||
if (!args.IsInDetailsRange || comp.Charges == comp.MaxCharges || !TryComp<AutoRechargeComponent>(uid, out var recharge))
|
||||
return;
|
||||
|
||||
var timeRemaining = Math.Round((recharge.NextChargeTime - _timing.CurTime).TotalSeconds);
|
||||
args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining)));
|
||||
}
|
||||
|
||||
public override void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
var startRecharge = comp.Charges == comp.MaxCharges;
|
||||
base.UseCharge(uid, comp);
|
||||
// start the recharge time after first use at full charge
|
||||
if (startRecharge && TryComp<AutoRechargeComponent>(uid, out var recharge))
|
||||
recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
|
||||
}
|
||||
}
|
||||
24
Content.Shared/Charges/Components/LimitedChargesComponent.cs
Normal file
24
Content.Shared/Charges/Components/LimitedChargesComponent.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.Charges.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Charges.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedChargesSystem))]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class LimitedChargesComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of charges
|
||||
/// </summary>
|
||||
[DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public int MaxCharges = 3;
|
||||
|
||||
/// <summary>
|
||||
/// The current number of charges
|
||||
/// </summary>
|
||||
[DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public int Charges = 3;
|
||||
}
|
||||
62
Content.Shared/Charges/Systems/SharedChargesSystem.cs
Normal file
62
Content.Shared/Charges/Systems/SharedChargesSystem.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Content.Shared.Charges.Components;
|
||||
using Content.Shared.Examine;
|
||||
|
||||
namespace Content.Shared.Charges.Systems;
|
||||
|
||||
public abstract class SharedChargesSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<LimitedChargesComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
protected virtual void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", comp.Charges)));
|
||||
if (comp.Charges == comp.MaxCharges)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a number of charges. If it over or underflows it will be clamped, wasting the extra charges.
|
||||
/// </summary>
|
||||
public void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
var old = comp.Charges;
|
||||
comp.Charges = Math.Clamp(comp.Charges + change, 0, comp.MaxCharges);
|
||||
if (comp.Charges != old)
|
||||
Dirty(comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the limited charges component and returns true if there are no charges. Will return false if there is no limited charges component.
|
||||
/// </summary>
|
||||
public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null)
|
||||
{
|
||||
// can't be empty if there are no limited charges
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return false;
|
||||
|
||||
return comp.Charges <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge.
|
||||
/// </summary>
|
||||
public virtual void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
|
||||
{
|
||||
if (Resolve(uid, ref comp, false))
|
||||
AddCharges(uid, -1, comp);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -9,62 +8,13 @@ namespace Content.Shared.Emag.Components;
|
||||
|
||||
[Access(typeof(EmagSystem))]
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed class EmagComponent : Component
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class EmagComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of charges the emag can have
|
||||
/// </summary>
|
||||
[DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaxCharges = 3;
|
||||
|
||||
/// <summary>
|
||||
/// The current number of charges on the emag
|
||||
/// </summary>
|
||||
[DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Charges = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the emag automatically recharges over time.
|
||||
/// </summary>
|
||||
[DataField("autoRecharge"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool AutoRecharge = true;
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to regain a single charge
|
||||
/// </summary>
|
||||
[DataField("rechargeDuration"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
|
||||
|
||||
/// <summary>
|
||||
/// The time when the next charge will be added
|
||||
/// </summary>
|
||||
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan NextChargeTime = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The tag that marks an entity as immune to emags
|
||||
/// </summary>
|
||||
[DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public string EmagImmuneTag = "EmagImmune";
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class EmagComponentState : ComponentState
|
||||
{
|
||||
public int MaxCharges;
|
||||
public int Charges;
|
||||
public bool AutoRecharge;
|
||||
public TimeSpan RechargeTime;
|
||||
public TimeSpan NextChargeTime;
|
||||
public string EmagImmuneTag;
|
||||
|
||||
public EmagComponentState(int maxCharges, int charges, TimeSpan rechargeTime, TimeSpan nextChargeTime, string emagImmuneTag, bool autoRecharge)
|
||||
{
|
||||
MaxCharges = maxCharges;
|
||||
Charges = charges;
|
||||
RechargeTime = rechargeTime;
|
||||
NextChargeTime = nextChargeTime;
|
||||
EmagImmuneTag = emagImmuneTag;
|
||||
AutoRecharge = autoRecharge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +1,63 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Charges.Components;
|
||||
using Content.Shared.Charges.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Emag.Systems
|
||||
namespace Content.Shared.Emag.Systems;
|
||||
|
||||
/// How to add an emag interaction:
|
||||
/// 1. Go to the system for the component you want the interaction with
|
||||
/// 2. Subscribe to the GotEmaggedEvent
|
||||
/// 3. Have some check for if this actually needs to be emagged or is already emagged (to stop charge waste)
|
||||
/// 4. Past the check, add all the effects you desire and HANDLE THE EVENT ARGUMENT so a charge is spent
|
||||
/// 5. Optionally, set Repeatable on the event to true if you don't want the emagged component to be added
|
||||
public sealed class EmagSystem : EntitySystem
|
||||
{
|
||||
/// How to add an emag interaction:
|
||||
/// 1. Go to the system for the component you want the interaction with
|
||||
/// 2. Subscribe to the GotEmaggedEvent
|
||||
/// 3. Have some check for if this actually needs to be emagged or is already emagged (to stop charge waste)
|
||||
/// 4. Past the check, add all the effects you desire and HANDLE THE EVENT ARGUMENT so a charge is spent
|
||||
public sealed class EmagSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly SharedChargesSystem _charges = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<EmagComponent, ExaminedEvent>(OnExamine);
|
||||
|
||||
SubscribeLocalEvent<EmagComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<EmagComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<EmagComponent, ComponentHandleState>(OnHandleState);
|
||||
SubscribeLocalEvent<EmagComponent, EntityUnpausedEvent>(OnUnpaused);
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, EmagComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new EmagComponentState(component.MaxCharges, component.Charges, component.RechargeDuration,
|
||||
component.NextChargeTime, component.EmagImmuneTag, component.AutoRecharge);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, EmagComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not EmagComponentState state)
|
||||
return;
|
||||
|
||||
component.MaxCharges = state.MaxCharges;
|
||||
component.Charges = state.Charges;
|
||||
component.RechargeDuration = state.RechargeTime;
|
||||
component.NextChargeTime = state.NextChargeTime;
|
||||
component.EmagImmuneTag = state.EmagImmuneTag;
|
||||
component.AutoRecharge = state.AutoRecharge;
|
||||
}
|
||||
|
||||
private void OnUnpaused(EntityUid uid, EmagComponent component, ref EntityUnpausedEvent args)
|
||||
{
|
||||
component.NextChargeTime += args.PausedTime;
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, EmagComponent component, ExaminedEvent args)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("emag-charges-remaining", ("charges", component.Charges)));
|
||||
if (component.Charges == component.MaxCharges)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("emag-max-charges"));
|
||||
return;
|
||||
}
|
||||
var timeRemaining = Math.Round((component.NextChargeTime - _timing.CurTime).TotalSeconds);
|
||||
args.PushMarkup(Loc.GetString("emag-recharging", ("seconds", timeRemaining)));
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var emag in EntityQuery<EmagComponent>())
|
||||
{
|
||||
if (!emag.AutoRecharge)
|
||||
continue;
|
||||
|
||||
if (emag.Charges == emag.MaxCharges)
|
||||
continue;
|
||||
|
||||
if (_timing.CurTime < emag.NextChargeTime)
|
||||
continue;
|
||||
|
||||
ChangeEmagCharge(emag.Owner, 1, true, emag);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, EmagComponent component, AfterInteractEvent args)
|
||||
private void OnAfterInteract(EntityUid uid, EmagComponent comp, AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach || args.Target is not { } target)
|
||||
return;
|
||||
|
||||
args.Handled = TryUseEmag(uid, args.User, target, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the charge on an emag.
|
||||
/// </summary>
|
||||
public bool ChangeEmagCharge(EntityUid uid, int change, bool resetTimer, EmagComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return false;
|
||||
|
||||
if (component.Charges + change < 0 || component.Charges + change > component.MaxCharges)
|
||||
return false;
|
||||
|
||||
if (resetTimer || component.Charges == component.MaxCharges)
|
||||
component.NextChargeTime = _timing.CurTime + component.RechargeDuration;
|
||||
|
||||
component.Charges += change;
|
||||
Dirty(component);
|
||||
return true;
|
||||
args.Handled = TryUseEmag(uid, args.User, target, comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to use the emag on a target entity
|
||||
/// </summary>
|
||||
public bool TryUseEmag(EntityUid emag, EntityUid user, EntityUid target, EmagComponent? component = null)
|
||||
public bool TryUseEmag(EntityUid uid, EntityUid user, EntityUid target, EmagComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(emag, ref component, false))
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return false;
|
||||
|
||||
if (_tagSystem.HasTag(target, component.EmagImmuneTag))
|
||||
if (_tag.HasTag(target, comp.EmagImmuneTag))
|
||||
return false;
|
||||
|
||||
if (component.Charges <= 0)
|
||||
TryComp<LimitedChargesComponent>(uid, out var charges);
|
||||
if (_charges.IsEmpty(uid, charges))
|
||||
{
|
||||
if (_net.IsServer)
|
||||
_popupSystem.PopupEntity(Loc.GetString("emag-no-charges"), user, user);
|
||||
if (_net.IsClient && _timing.IsFirstTimePredicted)
|
||||
_popup.PopupEntity(Loc.GetString("emag-no-charges"), user, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -142,13 +68,14 @@ namespace Content.Shared.Emag.Systems
|
||||
// only do popup on client
|
||||
if (_net.IsClient && _timing.IsFirstTimePredicted)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user,
|
||||
_popup.PopupEntity(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user,
|
||||
user, PopupType.Medium);
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target}");
|
||||
|
||||
ChangeEmagCharge(emag, -1, false, component);
|
||||
if (charges != null)
|
||||
_charges.UseCharge(uid, charges);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -168,8 +95,7 @@ namespace Content.Shared.Emag.Systems
|
||||
EnsureComp<EmaggedComponent>(target);
|
||||
return emaggedEvent.Handled;
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);
|
||||
|
||||
@@ -1,12 +1,2 @@
|
||||
emag-success = The card zaps something in {THE($target)}.
|
||||
emag-no-charges = No charges left!
|
||||
emag-charges-remaining = {$charges ->
|
||||
[one] It has [color=fuchsia]{$charges}[/color] charge remaining.
|
||||
*[other] It has [color=fuchsia]{$charges}[/color] charges remaining.
|
||||
}
|
||||
|
||||
emag-max-charges = It's at [color=green]maximum[/color] charges.
|
||||
emag-recharging = {$seconds ->
|
||||
[one] There is [color=yellow]{$seconds}[/color] second left until the next charge.
|
||||
*[other] There are [color=yellow]{$seconds}[/color] seconds left until the next charge.
|
||||
}
|
||||
10
Resources/Locale/en-US/limited-charges/limited-charges.ftl
Normal file
10
Resources/Locale/en-US/limited-charges/limited-charges.ftl
Normal file
@@ -0,0 +1,10 @@
|
||||
limited-charges-charges-remaining = {$charges ->
|
||||
[one] It has [color=fuchsia]{$charges}[/color] charge remaining.
|
||||
*[other] It has [color=fuchsia]{$charges}[/color] charges remaining.
|
||||
}
|
||||
|
||||
limited-charges-max-charges = It's at [color=green]maximum[/color] charges.
|
||||
limited-charges-recharging = {$seconds ->
|
||||
[one] There is [color=yellow]{$seconds}[/color] second left until the next charge.
|
||||
*[other] There are [color=yellow]{$seconds}[/color] seconds left until the next charge.
|
||||
}
|
||||
@@ -5,6 +5,21 @@
|
||||
description: The all-in-one hacking solution. The thinking man's lockpick. The iconic EMAG.
|
||||
components:
|
||||
- type: Emag
|
||||
- type: LimitedCharges
|
||||
- type: AutoRecharge
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
sprite: Objects/Tools/emag.rsi
|
||||
state: icon
|
||||
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: EmagUnlimited
|
||||
suffix: Unlimited
|
||||
name: cryptographic sequencer
|
||||
description: The all-in-one hacking solution. The thinking man's lockpick. The iconic EMAG.
|
||||
components:
|
||||
- type: Emag
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
sprite: Objects/Tools/emag.rsi
|
||||
|
||||
Reference in New Issue
Block a user