Important Decay Handling

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
It looks like the best way to make Rust++.cfg decay=false work sanely is to move it into Fougerite Core, take it away from Rust++. Because the only way to make it work sanely, is not to call any OnDecay hooks if decay=false and just remove decay damage on decay events. decay=false should mean no decay events are passed to plugins. Why would any server owner want to turn decay off, and then let a plugin do something on a decay event? If you want to manage decay, all the tools you need to do it are in Fougerite. You can generate your own decay event on any timetable you choose, by any criteria you desire. If there's something I've missed, please point it out.

But there is no sense putting the flag to turn off decay in a optional module. So I am considering putting that into Fougerite.cfg, and if decay=false, no OnDecay hooks will be called. Fougerite will just negate decay damage and move on. OnEntityHurt won't be called, either.

Would like some feedback before I press on. Thanks.
 

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
This is going to be good. I believe I have figured out the decay system in Rust top to bottom. You can do quite a lot with vanilla Rust and the server.cfg or rcon commands. With a mod like Fougerite, some elaborate decay schemes can be implemented. Here's some highlights:

  • Structure decay and Deployable item decay are independent processes. They run in parallel of course, like everything else that happens in the game. But they have different schedules and mechanisms. The only interaction between the two systems is when the deployable decay system skips an item that is attached to a structure part.
  • Yes, that means deployable items on a structure part don't decay at all. But they are destroyed when the structure part is destroyed. I got a plugin on my test server logging Decay events now. There won't be any attached to a structure. From EnvDecay class:
C#:
  protected EnvDecay.ThinkResult DecayThink()
  {
    if (!(bool) ((UnityEngine.Object) this._takeDamage))
      return EnvDecay.ThinkResult.Done;
    if ((bool) ((UnityEngine.Object) this._deployable) && (bool) ((UnityEngine.Object) this._deployable.GetCarrier()))
      return EnvDecay.ThinkResult.AgainLater;
    if ((double) (Time.time - this.lastDecayThink) < (double) decay.decaytickrate)
      return EnvDecay.ThinkResult.TooEarly;
    return this.CanApplyDecayDamage() && TakeDamage.HurtSelf((IDBase) this, (TakeDamage.Quantity) Hooks.EntityDecay((object) this._deployable, Mathf.Clamp(Time.time - this.lastDecayThink, 0.0f, decay.decaytickrate) / decay.deploy_maxhealth_sec * this._takeDamage.maxHealth * this.decayMultiplier), (object) null) == LifeStatus.WasKilled ? EnvDecay.ThinkResult.Done : EnvDecay.ThinkResult.AgainLater;
  }
It doesn't even get to check whether the decay.decaytickrate has elapsed before returning "AgainLater" (a flag value). It should be clear as day that deployable items on a structure never appear in Fougerite Decay events.
Next line gets UnityEngine.Time.time, which is server uptime in seconds, at the start of the current frame (game runs at 30fps). This is a float. A frame is basically one loop through the main engine loop, updating objects, positions, collision checks, etc. I hope everyone reading this recognizes what to set decay.decaytickrate to if you want deployed items on the ground not to decay: a very large number. How large can you set it? The property is a float. So, Single.MaxValue. Which is 3.402823 x 10^38. In seconds. Heat-death-of-the-Universe timescales. I have it set to the max value of a signed int32, which is 2147483647 seconds, a little over 68 years. The default value is 300.

So, that's how to turn off decay for deployable items. The method will return before decay.deploy_maxhealth_sec is referenced, so it doesn't matter what that is set to.

There are two more properties for decay. The source provides help text for the console system, so they are "self-documenting":
C#:
public class decay : ConsoleSystem
{
  [ConsoleSystem.Help("Number of seconds until decay deals max health amount of damage", "")]
  [ConsoleSystem.Admin]
  public static float deploy_maxhealth_sec = 43200f;
  [ConsoleSystem.Admin]
  [ConsoleSystem.Help("How often decay is processed", "")]
  public static float decaytickrate = 300f;
  [ConsoleSystem.Admin]
  [ConsoleSystem.Help("Maximum amount of env decays to process per frame. Use zero to process all env decays every frame", "")]
  public static int maxperframe = 100;
  [ConsoleSystem.Help("Maximum amount of env decays to process with raycasts. Use zero to process all env decays every frame", "")]
  [ConsoleSystem.Admin]
  public static int maxtestperframe = 8;
}
These are global variables. I don't think there is a way to set them per-object, but I'm still looking. If you have decaytickrate set to a large number, I recommend setting maxperframe to 1. Same goes for maxtestperframe. I wish it could be set to zero but according to the help text that will not have the intended effect. You can set these in server.cfg, from console rcon, on the command line.
 
Last edited:
  • Informative
Reactions: .phase

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
Here's the class for structure globals
C#:
public class structure : ConsoleSystem
{
  [ConsoleSystem.Admin]
  public static float minpercentdmg = 0.1f;
  [ConsoleSystem.Admin]
  public static int framelimit = 1;
  [ConsoleSystem.Admin]
  public static int maxframeattempt = 1000;

  [ConsoleSystem.Admin]
  public static void touchall(ref ConsoleSystem.Arg args)
  {
    using (List<StructureMaster>.Enumerator enumerator = StructureMaster.AllStructures.GetEnumerator())
    {
      while (enumerator.MoveNext())
      {
        StructureMaster current = enumerator.Current;
        if ((bool) ((Object) current))
          current.Touched();
      }
    }
  }
}
Or, in server.cfg and rcon console:
Code:
structure.framelimit
structure.maxframeattempt
structure.minpercentdmg
structure.touchall
Take a look at structure.touchall. It calls a method Touched(), and if you don't know what this console command does, it refreshes all structures on the server, just like it would when the owner enters, setting decay to the fresh state. What does Touched() do:
C#:
  public void Touched()
  {
    this._decayDelayRemaining = this.GetDecayDelay();
    StructureMaster.Schedule.Reschedule(this);
  }
I won't bore you with chasing GetDecayDelay. It gets the decay delay starting value according to material type. Metal lasts longer than Wood. Touched calls Reschedule:
C#:
    public static void Reschedule(StructureMaster master)
    {
      StaticQueue<StructureMaster.Schedule, StructureMaster>.requeue(master, ref master.regkey);
    }
Now, the requeue method is a member of Facepunch.Collections.StaticQueue. It's mind-numbingly dense and hard to read, probably because of the way dotPeek decompiles. I'm sure there's a much easier to use library overlying it. Like any .Net collection it has an iterator class for moving through the collection(s) in it.

So, when does StaticQueue get used? The NetCull class manages Callback hooks for updating state of game objects and has the static methods for instantiating and destroying them. Entity.Destroy() calls NetCull.Destroy(), for example. StructureMaster and EnvDecay classes register callbacks with NetCull when they are instantiated, mostly the same way Fougerite installs hooks for modules into its Hooks class. So it installs a UpdateFunctor type of hook, whatever that is, with RunDecayThink as an argument. This gets called for each StructureMaster, on the beforeEveryUpdate event. Now, look all the way down at the method named Process - which is called by RunDecayThink.
C#:
  private static class Callbacks
  {
    static Callbacks()
    {
      NetCull.Callbacks.beforeEveryUpdate += new NetCull.UpdateFunctor(StructureMaster.Callbacks.RunDecayThink);
    }

    private static void RunDecayThink()
    {
      try
      {
        StructureMaster.Schedule.Process(structure.maxframeattempt);
      }
      catch (Exception ex)
      {
        UnityEngine.Debug.LogException(ex);
      }
    }

    public static void Process(int maxCount = 1)
    {
      if (Globals.isLoading)
        return;
      try
      {
        StaticQueue<StructureMaster.Schedule, StructureMaster>.iterator iter = new StaticQueue<StructureMaster.Schedule, StructureMaster>.iterator(maxCount);
        int numDecaying = 0;
        StructureMaster v;
        bool flag = iter.Start(out v);
        while (flag)
          flag = !(bool) ((UnityEngine.Object) v) || !iter.Validate(ref v.regkey) ? iter.MissingNext(out v) : StructureMaster.Schedule.ThinkInstance(ref iter, ref v, ref v.regkey, ref numDecaying);
      }
      finally
      {
      }
    }
There's that StaticQueue thing, and Process gets its iterator with the argument maxCount, to size it. It's going to process this many on this call. RunDecayThink passed structure.maxframeattempt to it, a server global var. If it's not set, the default is 1000 as you can see in its source block. So, what if it was set to zero? There would be no elements, iter.Start would return false, and the loop would not run once. Looks like that's all there is to it.
The other structure global vars are used in the ThinkInstance method that's called inside the iterator loop. Don't care much what they are, since we can bypass the loop and stop decay that way, and save the server having to go through a loop again and again doing work that has no results. But, here's what they do:

C#:
// ThinkInstance first calls DoDecay and returns a decayStatus object
decayStatus = master.DoDecay();
// it's an enum with 4 members.  the return is one of these.
  protected enum DecayStatus
  {
    Decaying,
    Delaying,
    PentUpDecay,
    Gone,
  }
// in DoDecay, if we can set decayRate we can return Delaying
    if ((double) StructureMaster.decayRate <= 0.0)
      return StructureMaster.DecayStatus.Delaying;
// this num3 is amount of pent-up decay which accumulates, I think, when the game can't
// keep up with decay processing.  Too many structures?  This is the relief valve.  It's set to 0.1 by default
// could make it a big number and let decay get pent-up forever.
    if ((double) num3 < (double) structure.minpercentdmg)
      return StructureMaster.DecayStatus.PentUpDecay;
// ThinkInstance next passes the decay Status through a switch statement that selects an action for the
// interator.  Are you bored yet? Zzzzzzzzzzz
      if (iter.Next(ref key, cmd, out master))
        return numDecaying < structure.framelimit;
      else
        return false;
// and then it returns true or false back to the Process method.
False if the iterator has reached the end. True if the iterator can go to the next element AND numDecaying is less than another global var, structure.framelimit. numDecaying is initialized with value 0. So set this to 0 or -1, and ThinkInstance returns false every time. But if we got here, maxCount was at least 1, so we entered the while loop in the Process method. We got a valid object, so !(bool) StructureMaster is false. iter.Validate will be true on the 1st pass with 1 element, so it will return the result of iter.MissingNext which ought to be false on the 1st pass through a 1 element list. So, ThinkInstance doesn't get called on the last element, which is also the first element of a 1 element list. But it would get called if there was more than 1. And we can make it return false every time by setting structure.framelimit <= 0. Not that we need to.
The rest of ThinkInstance is a bunch of Raycasting at its components, and damage calculations that vary depending on whether it's a door, or a Pillar or Celing carrying weight and somebody was just totally overthinking this thing, I believe. Finally it calls our hook, which we patched in here:
C#:
              StructureComponent structureComponent = current;
              TakeDamage.Quantity damageQuantity = (TakeDamage.Quantity) dmg;
              Hooks.EntityDecay((object) current, dmg);
And that's it. Set structure.maxframeattempt = 0, and the method that calls our Decay hook is never entered, and no decay damage is ever applied to any Structure.
 
  • Winner
Reactions: .phase

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
And the server does a metric shit-ton less work every frame. Disabling structure decay ought to allow lots of structures.

Oh - by the way. You know those "List element has been changed, Enum blew itself to bits" exceptions that periodically dump all over your console?

It's the StaticQueue being molested by your plugins' On_Decay hooks. Doesn't happen when structure decay is disabled.

Another By the way: Rust++ decay=false only affects deployable object decay by setting decay.decaytickrate to Single.MaxValue. It does nothing to Structure decay.
 
  • Informative
Reactions: .phase

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
Told ya. It's Decay. Decay is fucked up in Rust. This exception is thrown by Rust alone. This exception occurs before our hook is called.
Code:
NullReferenceException: Object reference not set to an instance of an object
  at Facepunch.Collections.StaticQueue`2+iterator[EnvDecay+Schedule,EnvDecay].Start (.EnvDecay& v) [0x00000] in <filename unknown>:0

  at Facepunch.Collections.StaticQueue`2+iterator[EnvDecay+Schedule,EnvDecay].Next (Facepunch.Collections.Entry& prev_key, act cmd, .EnvDecay& v) [0x00000] in <filename unknown>:0

  at EnvDecay+Schedule.ThinkInstance (Facepunch.Collections.iterator& iter, .EnvDecay& decay, Facepunch.Collections.Entry& key, System.Int32& numLater) [0x00000] in <filename unknown>:0

  at EnvDecay+Schedule.Think (Int32 maxCount) [0x00000] in <filename unknown>:0

  at EnvDecay+Callbacks.RunDecayThink () [0x00000] in <filename unknown>:0
UnityEngine.Debug:Internal_LogException(Exception, Object)
UnityEngine.Debug:LogException(Exception)
Callbacks:RunDecayThink()
UpdateDelegate:Invoke()
Callbacks:FirePreUpdate(NetPreUpdate)
NetPreUpdate:LateUpdate()
It spams your console with this:
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
...

Soon afterwards, your server CPU utilization will start to increase. Either your GSP shuts it down, or it hangs on the next player connection attempt and spams the console with "Unresponsive for..." messages until you kill it or it takes down the PC.
 

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
Take a look at structure.touchall. It calls a method Touched(), and if you don't know what this console command does, it refreshes all structures on the server, just like it would when the owner enters, setting decay to the fresh state. What does Touched() do:
C#:
 public void Touched()
 {
   this._decayDelayRemaining = this.GetDecayDelay();
    StructureMaster.Schedule.Reschedule(this);
 }
That isn't quite right. What is does is reset the amount of decay remaining for the current decay cycle, putting off the next time structures are to take decay damage. Any decay damage already taken remains.
 

DreTaX

Probably knows the answer...
Administrator
Jun 29, 2014
4,093
4,784
113
At your house.
github.com
Told ya. It's Decay. Decay is fucked up in Rust. This exception is thrown by Rust alone. This exception occurs before our hook is called.
Code:
NullReferenceException: Object reference not set to an instance of an object
  at Facepunch.Collections.StaticQueue`2+iterator[EnvDecay+Schedule,EnvDecay].Start (.EnvDecay& v) [0x00000] in <filename unknown>:0

  at Facepunch.Collections.StaticQueue`2+iterator[EnvDecay+Schedule,EnvDecay].Next (Facepunch.Collections.Entry& prev_key, act cmd, .EnvDecay& v) [0x00000] in <filename unknown>:0

  at EnvDecay+Schedule.ThinkInstance (Facepunch.Collections.iterator& iter, .EnvDecay& decay, Facepunch.Collections.Entry& key, System.Int32& numLater) [0x00000] in <filename unknown>:0

  at EnvDecay+Schedule.Think (Int32 maxCount) [0x00000] in <filename unknown>:0

  at EnvDecay+Callbacks.RunDecayThink () [0x00000] in <filename unknown>:0
UnityEngine.Debug:Internal_LogException(Exception, Object)
UnityEngine.Debug:LogException(Exception)
Callbacks:RunDecayThink()
UpdateDelegate:Invoke()
Callbacks:FirePreUpdate(NetPreUpdate)
NetPreUpdate:LateUpdate()
It spams your console with this:
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object
...

Soon afterwards, your server CPU utilization will start to increase. Either your GSP shuts it down, or it hangs on the next player connection attempt and spams the console with "Unresponsive for..." messages until you kill it or it takes down the PC.
I guess we will have to write our own Decay system, which does dmg, if the player wasn't on for X days for some objects...
 

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
Well, he may have gotten it right eventually, but.....

Decay isn't hard to deal with if it is easy to find structures that haven't been used in a while. I have everything I need to make it easy for plugins to manage structures and deployables any way you want, and I've been working on a class for JintPlugin to do just that.

Meanwhile Wiper can do the job. It will wipe all of a player's structures just by hitting one of them.
 

mikec

Master Of All That I Survey
Retired Staff
Trusted Member
Jul 12, 2014
296
152
28
Los Angeles, California, USA
And that's it. Set structure.maxframeattempt = 0, and the method that calls our Decay hook is never entered, and no decay damage is ever applied to any Structure.
Unfortunately, the iterator constructor is fucking with me. Back to the drawing board.
C#:
      public iterator(int maxIter)
      {
        this = new StaticQueue<KEY, T>.iterator(maxIter, maxIter >= StaticQueue<KEY, T>.count ? 0 : StaticQueue<KEY, T>.count - maxIter);
      }

      public iterator(int maxIterations, int maxFailedIterations)
      {
        if (maxIterations == 0 || maxIterations > StaticQueue<KEY, T>.count)
        {
          this.attempts = StaticQueue<KEY, T>.count;
          this.fail_left = 0;
        }
        else if (maxIterations == StaticQueue<KEY, T>.count)
        {
          this.attempts = StaticQueue<KEY, T>.count;
          this.fail_left = 0;
        }
        else if (maxIterations + maxFailedIterations > StaticQueue<KEY, T>.count)
        {
          this.attempts = maxIterations;
          this.fail_left = StaticQueue<KEY, T>.count - maxIterations;
        }
        else
        {
          this.attempts = maxIterations;
          this.fail_left = maxFailedIterations;
        }
        this.position = 0;
        this.node = (StaticQueue<KEY, T>.node) null;
        this.next = !StaticQueue<KEY, T>.reg_made ? (StaticQueue<KEY, T>.node) null : StaticQueue<KEY, T>.reg.first;
      }
 
Last edited: