C#:
/*
* Copyright (c) 2026 Bazz3l
*
* Spinning Xmas Tree cannot be copied, edited and/or (re)distributed without the express permission of Bazz3l.
* Discord bazz3l
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
using System.Collections.Generic;
using System.Collections;
using Newtonsoft.Json;
using UnityEngine;
using System;
using VLB;
namespace Oxide.Plugins;
[Info("Spinning Xmas Tree", "Bazz3l", "1.3.1")]
[Description("Add decorations to your tree and watch it spin with awesomeness.")]
internal class SpinningXmasTree : RustPlugin
{
#region Fields
private const float INITIALIZE_DELAY_INTERVAL = 30f;
private readonly List<ChristmasTree> _christmasTrees = new();
private Coroutine _routine;
private ConfigData _configData;
#endregion
#region Config
private class ConfigData
{
[JsonProperty("required number of items to spin - (default: 2)")]
public int RequiredItemsToSpin { get; set; }
public static ConfigData DefaultConfig()
{
return new ConfigData
{
RequiredItemsToSpin = 2,
};
}
}
protected override void LoadDefaultConfig() => _configData = ConfigData.DefaultConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_configData = Config.ReadObject<ConfigData>() ?? throw new Exception();
}
catch (Exception e)
{
LoadDefaultConfig();
PrintWarning("Failed to load default config: {0}", e.Message);
}
}
protected override void SaveConfig() => Config.WriteObject(_configData, true);
#endregion
#region Oxide Hooks
private void OnServerInitialized() => StartupSetup();
private void Init() => Unsubscribe(nameof(OnItemAddedToContainer));
private void Unload()
{
CancelSetup();
RemoveSpinners();
}
private void OnEntitySpawned(ChristmasTree christmasTree)
{
if (christmasTree == null || christmasTree.IsDestroyed)
return;
NextFrame(() =>
{
if (christmasTree.IsDestroyed)
return;
CreateSpinner(christmasTree);
});
}
private void OnEntityKill(ChristmasTree christmasTree) => _christmasTrees.Remove(christmasTree);
private void OnItemAddedToContainer(ItemContainer container, Item item) => OnItemRemovedFromContainer(container, item);
private void OnItemRemovedFromContainer(ItemContainer container, Item item)
{
ChristmasTree christmasTree = container?.entityOwner as ChristmasTree;
if (christmasTree == null)
return;
int amount = container.itemList?.Count ?? 0;
christmasTree.GetComponent<SpinnerController>()?.TryActivateSpinner(amount, _configData.RequiredItemsToSpin);
}
#endregion
#region Spinner Management
private void StartupSetup()
{
CancelSetup();
_routine = ServerMgr.Instance.StartCoroutine(CreateSpinners());
}
private void CancelSetup()
{
if (_routine == null)
return;
ServerMgr.Instance.StopCoroutine(_routine);
_routine = null;
}
private IEnumerator CreateSpinners()
{
Puts("🎄🎄🎄please wait for all trees to initialize in {0} seconds🎄🎄🎄", INITIALIZE_DELAY_INTERVAL);
yield return CoroutineEx.waitForSeconds(INITIALIZE_DELAY_INTERVAL);
int count = 0;
foreach (BaseNetworkable nEntity in BaseNetworkable.serverEntities)
{
if (nEntity is not ChristmasTree christmasTree)
continue;
CreateSpinner(christmasTree);
if (count++ % 20 == 0)
{
yield return CoroutineEx.waitForEndOfFrame;
yield return CoroutineEx.waitForEndOfFrame;
}
}
Subscribe(nameof(OnItemAddedToContainer));
Puts("🎄🎄🎄all trees are now initialized, happy spinning 🎄🎄🎄");
_routine = null;
yield break;
}
private void RemoveSpinners()
{
if (Rust.Application.isQuitting)
return;
List<ChristmasTree> christmasTrees = new List<ChristmasTree>(_christmasTrees);
_christmasTrees.Clear();
foreach (ChristmasTree christmasTree in christmasTrees)
{
if (christmasTree == null)
continue;
RemoveSpinner(christmasTree);
}
}
private void CreateSpinner(ChristmasTree christmasTree)
{
if (christmasTree == null || christmasTree.IsDestroyed)
return;
christmasTree.syncPosition = true;
if (christmasTree.NetworkPosTickCallback == null)
{
Action action;
christmasTree.NetworkPosTickCallback = action = new Action(christmasTree.NetworkPositionTick);
}
_christmasTrees.Add(christmasTree);
christmasTree.InvokeRepeatingFixedTime(christmasTree.NetworkPosTickCallback);
SpinnerController controller = christmasTree.GetOrAddComponent<SpinnerController>();
controller.TryActivateSpinner(christmasTree.inventory?.itemList?.Count ?? 0, 2);
}
private void RemoveSpinner(ChristmasTree christmasTree)
{
if (christmasTree == null || christmasTree.IsDestroyed)
return;
christmasTree.CancelInvoke(christmasTree.NetworkPosTickCallback);
UnityEngine.Object.Destroy(christmasTree.GetComponent<SpinnerController>());
}
#endregion
#region Spinner Controller
private class SpinnerController : FacepunchBehaviour
{
public float spinSpeed = 1.0f;
public bool hasRequiredItems;
private void FixedUpdate()
{
if (!hasRequiredItems)
return;
transform.Rotate(0, Time.fixedDeltaTime * spinSpeed, 0);
transform.hasChanged = true;
}
public void TryActivateSpinner(int amount, int requiredAmount)
{
hasRequiredItems = requiredAmount == 0 || amount >= requiredAmount;
}
}
#endregion
}