Files
OpenRA/OpenRA.Mods.Common/Traits/Conditions/ExternalCondition.cs
RoosterDragon 36660b89e9 Silence IDE0290.
This rule recommends use of primary constructors. Apply the suggestions on simple POCOs, adjusting some to readonly/structs/records at the same time. For more complex classes, the use of primary constructors is more distracting than helpful, so silence the rule. IDEs can still offer fixes for using primary constructors, but it will not show up as a build issue.
2025-04-02 10:18:32 +03:00

232 lines
6.6 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[RequireExplicitImplementation]
public interface IConditionTimerWatcher
{
string Condition { get; }
void Update(int duration, int remaining);
}
[Desc("Allows a condition to be granted from an external source (Lua, warheads, etc).")]
public class ExternalConditionInfo : TraitInfo
{
[GrantedConditionReference]
[FieldLoader.Require]
public readonly string Condition = null;
[Desc("If > 0, restrict the number of times that this condition can be granted by a single source.")]
public readonly int SourceCap = 0;
[Desc("If > 0, restrict the number of times that this condition can be granted by any source.")]
public readonly int TotalCap = 0;
public override object Create(ActorInitializer init) { return new ExternalCondition(this); }
}
public class ExternalCondition : ITick, INotifyCreated, INotifyOwnerChanged
{
readonly struct TimedToken(int token, Actor self, object source, int duration)
{
public readonly int Expires = self.World.WorldTick + duration;
public readonly int Token = token;
public readonly object Source = source;
}
public readonly ExternalConditionInfo Info;
readonly Dictionary<object, HashSet<int>> permanentTokens = [];
// Tokens are sorted on insert/remove by ascending expiry time
readonly List<TimedToken> timedTokens = [];
IConditionTimerWatcher[] watchers;
int duration;
int expires;
public ExternalCondition(ExternalConditionInfo info)
{
Info = info;
}
public bool CanGrantCondition(object source)
{
if (source == null)
return false;
// Timed tokens do not count towards the source cap: the condition with the shortest
// remaining duration can always be revoked to make room.
if (Info.SourceCap > 0 &&
permanentTokens.TryGetValue(source, out var permanentTokensForSource) &&
permanentTokensForSource.Count >= Info.SourceCap)
return false;
if (Info.TotalCap > 0 &&
permanentTokens.Values.Sum(t => t.Count) >= Info.TotalCap)
return false;
return true;
}
public int GrantCondition(Actor self, object source, int duration = 0, int remaining = 0)
{
if (!CanGrantCondition(source))
return Actor.InvalidConditionToken;
var token = self.GrantCondition(Info.Condition);
permanentTokens.TryGetValue(source, out var permanent);
// Callers can override the amount of time remaining by passing a value
// between 1 and the duration
if (remaining <= 0 || remaining > duration)
remaining = duration;
if (duration > 0)
{
// Check level caps
if (Info.SourceCap > 0)
{
var timedCount = timedTokens.Count(t => t.Source == source);
if ((permanent?.Count ?? 0) + timedCount >= Info.SourceCap)
{
// Get timed token from the same source with closest expiration.
var expireIndex = timedTokens.FindIndex(t => t.Source == source);
if (expireIndex >= 0)
{
var expireToken = timedTokens[expireIndex].Token;
timedTokens.RemoveAt(expireIndex);
if (self.TokenValid(expireToken))
self.RevokeCondition(expireToken);
}
}
}
if (Info.TotalCap > 0)
{
var totalCount = permanentTokens.Values.Sum(t => t.Count) + timedTokens.Count;
if (totalCount >= Info.TotalCap && timedTokens.Count > 0)
{
var expire = timedTokens[0].Token;
if (self.TokenValid(expire))
self.RevokeCondition(expire);
timedTokens.RemoveAt(0);
}
}
var timedToken = new TimedToken(token, self, source, remaining);
var index = timedTokens.FindIndex(t => t.Expires >= timedToken.Expires);
if (index >= 0)
timedTokens.Insert(index, timedToken);
else
{
timedTokens.Add(timedToken);
// Track the duration and expiration for the longest remaining timer.
expires = timedToken.Expires;
this.duration = duration;
}
}
else if (permanent == null)
permanentTokens.Add(source, [token]);
else
permanent.Add(token);
return token;
}
/// <summary>Revokes the external condition with the given token if it was granted by this trait.</summary>
/// <returns><c>true</c> if the now-revoked condition was originally granted by this trait.</returns>
public bool TryRevokeCondition(Actor self, object source, int token)
{
if (source == null)
return false;
if (permanentTokens.TryGetValue(source, out var permanentTokensForSource))
{
if (!permanentTokensForSource.Remove(token))
return false;
}
else
{
var index = timedTokens.FindIndex(p => p.Token == token);
if (index >= 0 && timedTokens[index].Source == source)
timedTokens.RemoveAt(index);
else
return false;
}
if (self.TokenValid(token))
self.RevokeCondition(token);
return true;
}
void ITick.Tick(Actor self)
{
if (timedTokens.Count == 0)
return;
// Remove expired tokens
var worldTick = self.World.WorldTick;
var count = 0;
while (count < timedTokens.Count && timedTokens[count].Expires < worldTick)
{
var token = timedTokens[count].Token;
if (self.TokenValid(token))
self.RevokeCondition(token);
count++;
}
if (count > 0)
{
timedTokens.RemoveRange(0, count);
if (timedTokens.Count == 0)
{
// Notify watchers that all timers have expired.
foreach (var w in watchers)
w.Update(0, 0);
return;
}
}
// Watchers will be receiving notifications while the condition is enabled.
// They will also be provided with the number of ticks before the last timer ends,
// as well as the duration of the longest active instance.
if (timedTokens.Count > 0)
{
var remaining = expires - worldTick;
foreach (var w in watchers)
w.Update(duration, remaining);
}
}
bool Notifies(IConditionTimerWatcher watcher) { return watcher.Condition == Info.Condition; }
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{
foreach (var pair in self.World.ActorsWithTrait<INotifyProximityOwnerChanged>())
pair.Trait.OnProximityOwnerChanged(self, oldOwner, newOwner);
}
void INotifyCreated.Created(Actor self)
{
watchers = self.TraitsImplementing<IConditionTimerWatcher>().Where(Notifies).ToArray();
}
}
}