Pit Of Goblin

In this online horror co-op, you play as a scrappy goblin, scavenging dungeons, dodging traps, and fleeing monsters to feed the Goblin King-Queen. Team up with goblin pals, or go rogue, as you dive deep and cause chaos! Fail to feed them, and you might be the next meal!

SAVING SYSTEM

The game's saving system work purely on auto saves and allows three save slots. The game saves at certain "checkpoints" and is deleted after a failed attempt.
Only the host of a game get the savefile and while it does save character progress such as player inventories and upgrades, there are no individual player saves.
Save files have a validation check, for if a file is corrupted or out of date with the latest update.
There is also a basic obfuscation encryption available, but is currently disabled.

                    
public static class SaveLoad
{
    private static readonly int m_version = 1;
    public static readonly int Version = m_version;

    public enum FileValidation
    {
        Valid,
        Invalid,
        Corrupted,
        Empty
    }
    private static FileValidation m_validation = FileValidation.Valid;
    public static FileValidation Validation => m_validation;


    private static string m_directory = "/SaveFiles/";
    private static List<string> m_saveFiles = new List<string>()
    {
        "Save_1.sav", 
        "Save_2.sav", 
        "Save_3.sav",
    };


    private static int m_currentSaveFile = 0;
    public static int CurrentSaveFile
    {
        get => m_currentSaveFile;
        set => m_currentSaveFile = value;
    }

    public static List<string> SaveFiles => m_saveFiles;


    private static readonly string m_encryptionKey = "Pit0fG0blin";
    private static readonly bool m_isEncrypted = false;
    public static bool IsEncrypted => m_isEncrypted;


    private static bool m_prettyPrint = true;
    public static bool Prettyprint
    {
        get => m_prettyPrint;
        set => m_prettyPrint = value;
    }


    public static bool IsCurrentSaveFileValid()
    {
        if (ServiceLocator.GetService<GameStateService>().Current.State == GameStateAsset.GameState.DebugGame)
            return false;

        if (ServiceLocator.GetService<GameStateService>().IsRandomGame)
            return false;

        string fullDataPath = Application.persistentDataPath + m_directory + m_saveFiles[CurrentSaveFile];
        
        m_validation = ValidateSaveFile(fullDataPath, CurrentSaveFile);
        
        if (m_validation == FileValidation.Valid) return true;
        else return false;
    }

    public static bool IsSaveFileValid(string saveFile)
    {
        string fullDataPath = Application.persistentDataPath + m_directory + saveFile;
        int save = m_saveFiles.IndexOf(saveFile);

        m_validation = ValidateSaveFile(fullDataPath, save);
        
        if (m_validation == FileValidation.Valid) return true;
        else return false;
    }

    public static bool IsSaveFileValid(int saveFile)
    {
        if (saveFile == 3) return false;
        string fullDataPath = Application.persistentDataPath + m_directory + m_saveFiles[saveFile];
        
        m_validation = ValidateSaveFile(fullDataPath, saveFile);
        
        if (m_validation == FileValidation.Valid) return true;
        else return false;
    }


    public static bool IsSaveFileEmpty(int saveFile)
    {
        if (saveFile == 3) return false;
        string fullDataPath = Application.persistentDataPath + m_directory + m_saveFiles[saveFile];
        
        m_validation = ValidateSaveFile(fullDataPath, saveFile);

        if (m_validation == FileValidation.Empty) return true;
        else return false;
    }
    public static bool IsSaveFileEmpty(string saveFile)
    {
        string fullDataPath = Application.persistentDataPath + m_directory + saveFile;
        int save = m_saveFiles.IndexOf(saveFile);

        m_validation = ValidateSaveFile(fullDataPath, save);

        if (m_validation == FileValidation.Empty) return true;
        else return false;
    }



    public static void Save(SaveData saveData)
    {
        string directory = Application.persistentDataPath + m_directory;

        GUIUtility.systemCopyBuffer = directory;

        if (!Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        var fileDate = DateTime.Now.ToString("yyyy-MM-dd");
        saveData.SaveFileCreationDate = fileDate;

        string json = JsonUtility.ToJson(saveData, m_prettyPrint);
        string finalJson = "";

        if (IsEncrypted)
        {
            string encryptedJson = EncryptDecrypt(json);
            finalJson = encryptedJson;
        }
        else
        {
            finalJson = json;
        }

        File.WriteAllText(directory + m_saveFiles[CurrentSaveFile], finalJson);

        Debug.Log("SAVING GAME!");
    }

    public static SaveData Load()
    {
        string fullDataPath = Application.persistentDataPath + m_directory + m_saveFiles[CurrentSaveFile];
        SaveData saveData = new SaveData();

        m_validation = ValidateSaveFile(fullDataPath, CurrentSaveFile);

        if (m_validation == FileValidation.Valid)
        {
            string json = File.ReadAllText(fullDataPath);
            string finalJson = "";

            if (IsEncrypted)
            {
                finalJson = EncryptDecrypt(json);
            }
            else
            {
                finalJson = json;
            }

            saveData = JsonUtility.FromJson<SaveData>(finalJson);

            Debug.Log("LOADING GAME!");

            return saveData;
        }

        Debug.LogError($"Validation Failed! -- Could not Load Game -- SaveData {CurrentSaveFile} will be set to Null!");
        return null;

    }

    public static SaveData GetSelectedSaveData(int saveFile)
    {
        string fullDataPath = Application.persistentDataPath + m_directory + m_saveFiles[saveFile];
        SaveData saveData = new SaveData();

        m_validation = ValidateSaveFile(fullDataPath, saveFile);

        if (m_validation == FileValidation.Valid)
        {
            string json = File.ReadAllText(fullDataPath);
            string finalJson = "";

            if (IsEncrypted)
            {
                finalJson = EncryptDecrypt(json);
            }
            else
            {
                finalJson = json;
            }

            saveData = JsonUtility.FromJson<SaveData>(json);

            return saveData;
        }

        Debug.LogError($"Validation Failed! -- Could not Get Selected({saveFile + 1}) SaveData -- SaveData Set to Null!");
        return null;
    }

    public static void Delete()
    {
        string fullDataPath = Application.persistentDataPath + m_directory + m_saveFiles[CurrentSaveFile];
    
        if (File.Exists(fullDataPath))
        {
            File.Delete(fullDataPath);

            Debug.Log("SAVE FILE DELETED!");
        }
    }

    private static string EncryptDecrypt(string data)
    {
        string result = "";

        for (int i = 0; i < data.Length; i++)
        {
            result += (char) (data[i] ^ m_encryptionKey[i % m_encryptionKey.Length]);
        }

        return result;
    }


    private static FileValidation ValidateSaveFile(string fullDataPath, int save)
    {
        if (!File.Exists(fullDataPath))
        {
            return FileValidation.Empty;
        }

        SaveData saveData = new SaveData();

        string json = File.ReadAllText(fullDataPath);
        string finalJson = "";

        if (IsEncrypted)
        {
            finalJson = EncryptDecrypt(json);
        }
        else
        {
            finalJson = json;
        }

        if (finalJson == "")
        {
            Debug.LogError($"Validation Failed! -- JSON SaveData {save + 1} is Null!");
            return FileValidation.Corrupted;
        }

        try
        {
            saveData = JsonUtility.FromJson<SaveData>(finalJson);
        }
        catch (Exception e)
        {
            Debug.LogError($"Validation Failed! -- Save File {save + 1} is Corrupted! -- Exception: {e}");
            return FileValidation.Corrupted;
        }

        if (saveData.Version != Version)
        {
            if (saveData.Version > Version)
            {
                Debug.LogError($"Validation Failed! -- Save File {save + 1} Version Out of Date -- Current Version: {saveData.Version}, Expected Version: {Version}");
            }
            else if(saveData.Version < Version)
            {
                Debug.LogError($"Validation Failed! -- Save File {save + 1} Version Out of Date -- Current Version: {saveData.Version}, Expected Version: {Version}");
            }

            return FileValidation.Invalid;
        }

        return FileValidation.Valid;
    }
}
                    
                

SAVEDATA

The SaveData class keeps track of all the different values that are saved.
Whenever there is an auto save, the savedata of the current save is updated with new values.
Before a new save, values are set in a gamemode_goblin script that updates whenever new progress i made.

RAGDOLL ITEM

The main purpose of this feature is to be able to pick up ragdolled or dead entities, for example, bringing back a dead player to revive.
Items and ragdolls already existed, I helped implement a new item that used a dynamic mesh instead of a static one.
Some things to keep in mind working on this feature was: how to handle the entity linked to the item, the appearance, equipment and state of the entity that's picked up, and how to synch the state and position of the entity and item.
Along with this dynamic item I worked on how to revive the entity using it's ragdoll item.

                    
public class CorpseItem : NetworkBehaviour, ISoulHolder
{
    public InventoryItem InventoryItem { get; private set; }
    /// <summary>
    /// This bool indicates that the corpse has been initialized, and network synchronized and
    /// is ready for use by other objects.
    /// CorpseItem is not safe to use before this is true.
    /// <summary>
    public bool IsReady { get; private set; }

    [SerializeField]
    private float m_breakFreeTime = 5.0f;

    [SerializeField] 
    private Rigidbody m_itemRigidbody;

    private Vector3 m_lastPosition = Vector3.zero;
    private Transform m_heldTransformRagdollHolder;
    
    public Entity Entity { get; set; }
    public Ragdoll Ragdoll => Entity.Ragdoll;

    public bool HasSoul
    {
        get => Entity.HasSoul;
        set => Entity.HasSoul = value;
    }

    private PlayerController m_playerController;

    private void Awake()
    {
        InventoryItem = GetComponent<InventoryItem>();
    }
    public NetworkObjectReference NetworkSyncEntity { get; set; }
    protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
    {
        if (serializer.IsWriter)
        {
            var writer = serializer.GetFastBufferWriter();
            writer.WriteNetworkSerializable<NetworkObjectReference>(Entity.NetworkObject);
        }
        else if (serializer.IsReader)
        {
            var reader = serializer.GetFastBufferReader();
            reader.ReadNetworkSerializable(out NetworkObjectReference entity);
            NetworkSyncEntity = entity;
        }
    }

    [Rpc(SendTo.Everyone)]
    public void SetEntityRpc(NetworkObjectReference entityRef)
    {
        NetworkSyncEntity = entityRef;
    }

    private IEnumerator InitWhenReady()
    {
        yield return new WaitUntil(() =>
        {
            if (!NetworkSyncEntity.TryGet(out var netObj))
                return Entity != null;
            if (!netObj.TryGetComponent<Entity>(out var entity))
                return Entity != null;
            Entity = entity;
            return Entity != null;
        });
        LocalInit(Entity);
        DontDestroyOnLoad(this.gameObject);
    }

    public override void OnNetworkSpawn()
    {
        InventoryItem.OnPickup += OnPickup;
        InventoryItem.OnPlaceInHand += OnPlaceInHand;
        InventoryItem.OnDrop += OnDrop;
        InventoryItem.OnItemVisible += OnItemVisible;
        StartCoroutine(InitWhenReady());
    }

    public override void OnNetworkDespawn()
    {
        InventoryItem.OnPickup -= OnPickup;
        InventoryItem.OnPlaceInHand -= OnPlaceInHand;
        InventoryItem.OnDrop -= OnDrop;
        InventoryItem.OnItemVisible -= OnItemVisible;

        Ragdoll.SetRagdollLayer(Ragdoll.RagdollAliveLayerMask);
        Ragdoll.ReleasePose();

        if (Entity != null)
        {
            Entity.OnRagdollEnabledAction -= OnRagdollEnabledAction;
            Entity.OnRagdollDisabledAction -= OnRagdollDisabledAction;
            Entity.CorpseItem = null;
        }
    }

    public void LocalInit(Entity entity)
    {
        Entity = entity;
        Entity.CorpseItem = this;
        Entity.RagdollInteract.CorpseItem = InventoryItem;
        Entity.OnRagdollEnabledAction += OnRagdollEnabledAction;
        Entity.OnRagdollDisabledAction += OnRagdollDisabledAction;

        transform.position = Entity.transform.position;
        transform.rotation = Entity.transform.rotation;
        m_heldTransformRagdollHolder = InventoryItem.HeldTransform.GetChild(1);
        InventoryItem.IsInteractable = false;
        IsReady = true;
    }

    private void OnRagdollDisabledAction(Entity obj)
    {
        InventoryItem.IsInteractable = false;
        InventoryItem.ItemRigidBody.isKinematic = false;
    }

    private void OnRagdollEnabledAction(Entity obj)
    {
        Entity.StandupTime = NetworkManager.Singleton.ServerTime.TimeAsFloat + 2.0f;
        InventoryItem.IsInteractable = true;
        InventoryItem.ItemRigidBody.isKinematic = true;
        InventoryItem.ItemRigidBody.MovePosition(Entity.transform.position);
        InventoryItem.ItemRigidBody.MoveRotation(Entity.transform.rotation);
    }

    private PoseType m_lastBakedPose = PoseType.Default;
    public void RefreshMeshInfo(GripSlot slot)
    {
        PoseType poseToBake = PoseType.Default;
        if (slot == GripSlot.Backpack)
        {
            poseToBake = PoseType.Backpack;
        }
        else if (slot == GripSlot.ItemRack)
        {
            poseToBake = PoseType.ItemRack;
        }
        else
        {
            poseToBake = PoseType.ItemRack;
            Debug.LogError($"Placed corpse {this.gameObject.name} in unsupported GripSlot {slot}", this.gameObject);
        }

        bool doBakePose = false;
        if (poseToBake != m_lastBakedPose)
        {
            Ragdoll.SetBakePose(poseToBake);
            m_lastBakedPose =  poseToBake;
            doBakePose = true;
        }

        if (doBakePose)
        {
            InventoryItem.MeshInfo.Clear();
            var targets = Entity.Outline.OutlineTargets;
            foreach (var target in targets)
            {
                if (target.Renderer is SkinnedMeshRenderer smr)
                {
                    var meshInfo = new InventoryItem.ItemMeshInfo();
                    Mesh mesh = new Mesh();
                    smr.BakeMesh(mesh);
                    meshInfo.m_mesh = mesh;
                    var smrTrans = smr.transform.localToWorldMatrix;
                    smrTrans.SetTRS(smrTrans.GetPosition(), smrTrans.rotation, Vector3.one);
                    meshInfo.m_transform = transform.localToWorldMatrix.inverse * smrTrans;
                    InventoryItem.MeshInfo.Add(meshInfo);
                }
            }
        }
    }

    private void OnItemVisible(bool visible)
    {
        if (visible)
        {
            Entity.ShowThirdPersonVisuals();
        }
        else
        {
            Entity.HideVisuals();
        }
    }

    private void OnPlaceInHand(InventoryItem item)
    {
        SetPose(PoseType.BeingCarried);
    }

    private void SetPose(PoseType poseType)
    {
        if (InventoryItem.IsInHands || InventoryItem.IsInObjectPlace)
        {
            var hipRigidbody = Ragdoll.GetRagdollHip();
            
            if (InventoryItem.Entity != null && InventoryItem.Entity.IsLocalPlayerEntity)
            {
                hipRigidbody.transform.position = m_heldTransformRagdollHolder.position;
                hipRigidbody.transform.rotation = m_heldTransformRagdollHolder.rotation;
                Physics.SyncTransforms();
            }
            else
            {
                if (InventoryItem.IsInHands)
                {
                    hipRigidbody.transform.position = InventoryItem.PickupTransform.position;
                    hipRigidbody.transform.rotation = InventoryItem.PickupTransform.rotation;
                    Physics.SyncTransforms();
                }
                else
                {
                    hipRigidbody.transform.position = m_heldTransformRagdollHolder.position;
                    hipRigidbody.transform.rotation = m_heldTransformRagdollHolder.rotation;
                    Physics.SyncTransforms();
                }
            }
        }
        Ragdoll.SetPose(poseType);
        Physics.SyncTransforms();
    }

    private void Update()
    {
        if (!IsReady)
            return;
        
        if (Entity.IsDead == false && Entity.IsPickedUp && m_playerController != null && (float)NetworkManager.ServerTime.Time >= m_pickupServerTime + m_breakFreeTime)
        {
            m_playerController.BreakFreeInput();
        }
    }

    private void LateUpdate()
    {
        if (!IsReady)
            return;
        if (InventoryItem.IsInHands || InventoryItem.IsInObjectPlace)
        {
            var hipRigidbody = Ragdoll.GetRagdollHip();
            
            if (InventoryItem.Entity != null && InventoryItem.Entity.IsLocalPlayerEntity)
            {
                hipRigidbody.transform.position = m_heldTransformRagdollHolder.position;
                hipRigidbody.transform.rotation = m_heldTransformRagdollHolder.rotation;
                
                Physics.SyncTransforms();
                
            }
            else
            {
                if (InventoryItem.IsInHands)
                {
                    hipRigidbody.transform.position = InventoryItem.PickupTransform.position;
                    hipRigidbody.transform.rotation = InventoryItem.PickupTransform.rotation;
                    Physics.SyncTransforms();
                }
                else
                {
                    hipRigidbody.transform.position = m_heldTransformRagdollHolder.position;
                    hipRigidbody.transform.rotation = m_heldTransformRagdollHolder.rotation;
                    Physics.SyncTransforms();
                }
            }
        }
        else
        {
            var hipRigidBody = Ragdoll.GetHipRigidbody();
            if (hipRigidBody != null)
            {
                transform.position = hipRigidBody.position;
                transform.rotation = hipRigidBody.rotation;
            }
        }
        
        if (!Ragdoll.IsRagdolled)
        {
            if (Entity != null)
            {
                transform.position = Entity.transform.position;
                transform.rotation = Entity.transform.rotation;
            }
            
        }
    }

    private bool ShouldApplyPhysics()
    {
        if (InventoryItem.IsInHands)
            return true;
        if (InventoryItem.IsInObjectPlace)
        {
            if (InventoryItem.AttachedSlot.ParentItem != null && InventoryItem.AttachedSlot.ParentItem.Entity != null)
                return true;
        }

        return false;
    }

    private void FixedUpdate()
    {
        if (!IsReady)
            return;
        if (ShouldApplyPhysics())
        {
            Vector3 deltaPosition = Vector3.zero;
            if (InventoryItem.IsOwner)
            {
                deltaPosition = m_heldTransformRagdollHolder.position - m_lastPosition;
                m_lastPosition = m_heldTransformRagdollHolder.position;
            }
            else
            {
                deltaPosition = InventoryItem.PickupTransform.position - m_lastPosition;
                m_lastPosition = InventoryItem.PickupTransform.position;
            }
            
            if (InventoryItem.IsInHands && InventoryItem.Entity.IsLocalPlayerEntity) 
                Ragdoll.RagdollAnimator.User_AddAllBonesImpact(deltaPosition * (-200f), mode:ForceMode.Force);
            else
                Ragdoll.RagdollAnimator.User_AddAllBonesImpact(deltaPosition * (-600f), mode:ForceMode.Force);
        }
    }

    private float m_pickupServerTime = 0.0f;
    public void OnPickup(InventoryItem item)
    {
        Entity.EnableRagdoll();
        Entity.LockRagdoll();
        Entity.IsPickedUp = true;

        if (Entity.IsOwner && Entity is EntityPlayer ep)
        {
            m_playerController = ep.PlayerController;
            //m_playerController.SetSpectatorCam();
        }
            

        m_pickupServerTime = (float)NetworkManager.ServerTime.Time;
        
        if(InventoryItem.IsOwner)
            m_lastPosition = m_heldTransformRagdollHolder.position;
        else
            m_lastPosition = InventoryItem.PickupTransform.position;
    }

    public void PlaceInSlot(GripSlot slot)
    {
        if (Entity == null)
            return;
        if (slot == GripSlot.Backpack)
        {
            SetPose(PoseType.Backpack);
        }
        else if (slot == GripSlot.ItemRack)
        {
            SetPose(PoseType.ItemRack);
        }
        else
        {
            SetPose(PoseType.ItemRack);
            Debug.LogError($"Placed corpse {this.gameObject.name} in unsupported GripSlot {slot}", this.gameObject);
        }
            
        OnPickup(InventoryItem);
    }

    private void OnDrop(InventoryItem item)
    {
        Entity.StandupTime = NetworkManager.Singleton.ServerTime.TimeAsFloat + 2.0f;
        SetPose(PoseType.Default);
        Ragdoll.ReleasePose();
        if (Entity == null)
            return;
        Entity.IsPickedUp = false;
    }

    public void AddForce(Vector3 force, Vector3 rotationForce)
    {
        //  Adjusting force to work better with corpses
        RPCProxy.Entity_ThrowAsRagdollRpc(this.Entity, force * 2f, duration: 1.0f);
    }
}
                    
                

RAGDOLL INTERACT

The ragdoll interact class is inherited from a Interactable base class and is a component on a entities corpse item.
It only exists on entities that are able to be picked up and checks for if the ragdolled entity can be interacted with.

OBJECT PLACE

The Object place script is used to place an items in specified spot.
It is for example used when placing items on a backpack, or placing a valuable item on a table to unlock a new area.
I reworked this a bit to work with flagged values and the backpack.
An object place can be specified on how it should funtion. For example, if it can be removed, the type of item, the size of the item, or a specific item flag.

                    
public class ObjectPlaceInteract : NetworkInteractableBase
{
    [SerializeField] private int m_priority = 3;
    public override int Priority => m_priority;

    [SerializeField] private bool m_canBeRemoved = true;
    [Tooltip("Will set which way to point any light attached to this object place")]
    [SerializeField] private Transform m_lightGuide;

    [Space(5)]

    [Header("Allowed items")]
    [SerializeField] private bool m_allowSmallItems = true;
    [SerializeField] private bool m_allowBigItems = true;

    [Space(2)]

    [SerializeField] private bool m_allowAnyItem = true;
    [SerializeField] private ItemType m_allowedTypes;
    [SerializeField] private List<string> m_allowedIDs;

    private Vector3 m_defaultItemScale = Vector3.one;
    private Vector3 m_currentItemScale;
    private float m_bigItemBackpackScale = 0.5f;
    private float m_smallItemBackpackScale = 0.75f;


    [Space(5)]

    [SerializeField] private Material m_drawMaterial;

    [Space(5)]

    [SerializeField] private bool m_persistent = false;

    [ShowIf("@m_persistent==true")]
    [SerializeField] private GameObject m_defaultItem;


    [SerializeField] private string m_flag;
    public string Flag
    {
        get => m_flag;
        set => m_flag = value;
    }


    [Space(5)]

    [SerializeField] private SoundEvent m_onPlaceSound;

    [Space(2)]

    public UnityEvent<InventoryItem> OnPlaceItem;
    public UnityEvent<InventoryItem> OnRemoveItem;

    [Space(2)]

    [Header("Destroy When Placed")]
    public bool m_destroyItem = false;
    public bool m_destroyPlayVFX = false;


    private InventoryItem m_localSelectedItem;

    public InventoryItem LocalSelectedItem
    {
        get => m_localSelectedItem;
        set => m_localSelectedItem = value;
    }

    public bool HasAttachedItem => AttachedItem != null;

    public RigAttachmentSlot AttachmentSlot { get; private set; }

    public InventoryItem AttachedItem
    {
        get
        {
            if (AttachmentSlot == null)
                return null;
            return AttachmentSlot.AttachedGrip == null
                ? null
                : AttachmentSlot.AttachedGrip.GetComponentInParent<InventoryItem>();
        }
    }    

    private bool m_selected = false;
    private bool m_disableInteract = false;

    public bool DisableInteract
    {
        get => m_disableInteract;
        set => m_disableInteract = value;
    }

    public bool AllowBigItems
    {
        get => m_allowBigItems;
        set => m_allowBigItems = value;
    }
    public bool AllowSmallItems
    {
        get => m_allowSmallItems;
        set => m_allowSmallItems = value;
    }
    public bool AllowAnyItem
    {
        get => m_allowAnyItem;
        set => m_allowAnyItem = value;
    }

    public IReadOnlyList<string> AllowedIDs => m_allowedIDs;
    
    public bool IsBackpackSlot => m_backpack != null;
    private ItemBackpack m_backpack;

    protected override void Awake()
    {
        base.Awake();
        m_backpack = GetComponentInParent<ItemBackpack>();
        AttachmentSlot = GetComponentInChildren<RigAttachmentSlot>();
    }

    private NetworkObjectReference m_queuedObjectReference;
    protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
    {
        if (serializer.IsWriter)
        {   
            NetworkObjectReference r = HasAttachedItem ? AttachedItem.NetworkObject : new NetworkObjectReference((GameObject)null);
            serializer.SerializeValue(ref r);
        }
        else if (serializer.IsReader)
        {
            serializer.SerializeValue(ref m_queuedObjectReference);
        }
    }

    protected virtual void Start()
    {
    }

    public override void OnNetworkDespawn()
    {
        GameMode_Goblin.Instance.OnFlagSet -= OnSetPersistentFlag;
        GameMode_Goblin.Instance.OnFlagUnset -= OnUnsetPersistentFlag;
    }

    protected override void OnNetworkPostSpawn()
    {
        if (m_persistent)
        {
            if (m_defaultItem == null) return;
            
            StartCoroutine(WaitForGameMode());
        }
    }

    protected override void OnNetworkSessionSynchronized()
    {
        base.OnNetworkSessionSynchronized();
        if (m_queuedObjectReference.TryGet(out var netObj))
        {
            if (netObj.TryGetComponent(out CorpseItem corpseItem))
            {
                StartCoroutine(WaitAndAttach(corpseItem));
            }
            else if (netObj.TryGetComponent(out InventoryItem item))
            {
                AttachItemLocally(item, false);
            }
        }
    }

    private IEnumerator WaitAndAttach(CorpseItem corpseItem)
    {
        yield return new WaitWhile(() => !corpseItem.IsReady);
        AttachItemLocally(corpseItem.InventoryItem, false);
    }

    private IEnumerator WaitForGameMode()
    {
        yield return new WaitWhile(() => GameMode_Goblin.Instance == null || NetworkHandler.Singleton.IsLoadingScene);

        GameMode_Goblin.Instance.OnFlagSet += OnSetPersistentFlag;
        GameMode_Goblin.Instance.OnFlagUnset += OnUnsetPersistentFlag;

        if (GameMode_Goblin.Instance.HasFlag(m_flag))
        {
            AddAttachedPersistentItem();
        }
    }

    private void OnSetPersistentFlag(string flag)
    {
        if (flag == m_flag && m_persistent) AddAttachedPersistentItem();
    }

    private void OnUnsetPersistentFlag(string flag)
    {
        if (flag == m_flag && m_persistent) RemoveAttachedPersistentItem();
    }


    private void AddAttachedPersistentItem()
    {
        if (HasAttachedItem) return;

        var itemData = GameMode_Goblin.Instance.ObjectFlagsPlaced.GetValueOrDefault(m_flag);

        var networkPrefabs = NetworkHandler.Singleton.NetworkPrefabsList.PrefabList;

        if (m_defaultItem.TryGetComponent(out NetworkObject defaultNetObj) && itemData.ObjectHash == 0)
        {
            var instance = NetworkManager.SpawnManager.InstantiateAndSpawn(defaultNetObj);
            AttachItem(instance.GetComponent<InventoryItem>(), false);
        }
        else
        {
            foreach (var np in networkPrefabs)
            {
                if (np == null || np.Prefab == null || !np.Prefab.TryGetComponent<NetworkObject>(out var netObj)) continue;
                if (itemData.ObjectHash == netObj.PrefabIdHash)
                {
                    var instance = NetworkManager.SpawnManager.InstantiateAndSpawn(netObj);
                    AttachItem(instance.GetComponent<InventoryItem>(), false);
                }
            }
        }
    }

    private void RemoveAttachedPersistentItem()
    {
        if (!IsServer)
            return;
        if (AttachedItem == null)
            return;
        
        AttachedItem.NetworkObject.Despawn();
    }

    public void AttachItem(InventoryItem item, bool playAudio)
    {
        SetSelection(false);

        AttachItemRpc(item.NetworkObject, playAudio);

        if (m_destroyItem)
        {
            DetachItemRpc();
            item.ConsumeItemRpc(m_destroyPlayVFX);
        }
    }

    private void SetPersistence()
    {
        // Set persistence flag
        if (m_persistent && !string.IsNullOrEmpty(m_flag))
        {
            AddItemToObjectFlagsPlaced();
            GameMode_Goblin.Instance.SetFlag(m_flag);
        }
    }

    public void SetObjectFlag(ItemData itemData, string flag)
    {
        if (!m_persistent && m_canBeRemoved && !string.IsNullOrEmpty(flag))
        {
            GameMode_Goblin.Instance.AddObjectFlagsPlaced(flag, itemData);
            GameMode_Goblin.Instance.SetFlag(flag);
        }
    }

    public void RemoveObjectFlag()
    {
        if (!m_persistent && m_canBeRemoved && !string.IsNullOrEmpty(m_flag))
        {
            GameMode_Goblin.Instance.RemoveObjectFlagsPlaced(m_flag);
            GameMode_Goblin.Instance.UnsetFlag(m_flag);
        }
    }

    private void AddItemToObjectFlagsPlaced()
    {
        var itemData = new ItemData()
        {
            ObjectHash = AttachedItem.NetworkObject.PrefabIdHash,
            Position = AttachedItem.transform.position,
            Rotation = AttachedItem.transform.rotation,

            Name = AttachedItem.name,
        };
        GameMode_Goblin.Instance.AddObjectFlagsPlaced(m_flag, itemData);
    }


    [Rpc(SendTo.Everyone)]
    private void AttachItemRpc(NetworkObjectReference itemRef, bool playAudio)
    {
        if (!itemRef.TryGet(out var networkObject))
        {
            Debug.LogError("Failed to attach Item to ObjectPlaceInteract!", this.gameObject);
            return;
        }
        var item = networkObject.GetComponent<InventoryItem>();
        if (item == null)
        {
            Debug.LogError("Failed to attach item to ObjectPlaceInteract! Object is not an item!", this.gameObject);
            return;
        }
        AttachItemLocally(item, playAudio);
    }

    private void AttachItemLocally(InventoryItem item, bool playAudio)
    {
        if (item == null)
        {
            Debug.LogError("Attach Item is null, this happens when client is trying to attach a persistent ObjectPlace, but I think it's fine", gameObject);
            return;
        }
        
        if (!m_canBeRemoved) 
            item.IsInteractable = false;
        item.AttachToSlot(AttachmentSlot);
        
        if (IsBackpackSlot)
        {
            if (item.TryGetComponent(out CorpseItem c))
            {
            }
            else
            {
                m_currentItemScale = m_defaultItemScale;
                if (item.IsBig) m_currentItemScale = m_defaultItemScale * m_bigItemBackpackScale;
                else m_currentItemScale = m_defaultItemScale * m_smallItemBackpackScale;

                item.transform.localScale = m_currentItemScale;
            }

            item.SetItemPriority(4);
            if (m_lightGuide != null)
                item.LightFollow = m_lightGuide;

            //  Todo: Fix so that IsInInventory is only true when Inventory isnt null!!
            if (IsBackpackSlot && m_backpack.IsInInventory && m_backpack.Inventory != null && m_backpack.Inventory.Owner.IsLocalPlayerEntity)
                item.SetVisibility(false);
        }

        if (item.Corpse != null)
        {
            item.Corpse.PlaceInSlot(AttachmentSlot.TargetGrip);
            if (item.Corpse.Entity != null)
                item.Corpse.Entity.OnTakeDamageAction += OnTakeDamageAction;
        }
        
        SetPersistence();
        
        item.OnPickup += OnPickup;
        OnPlaceItem.Invoke(item);

        if (playAudio && m_onPlaceSound != null)
        {
            m_onPlaceSound.Play(transform);
        }
    }

    private void OnTakeDamageAction(Entity arg1, float arg2)
    {
        DetachItemRpc();
    }

    /// <summary>
    /// FF-Whuop:
    /// Special function ONLY used by EpicGrill, do NOT use this for anything else!
    /// Epic grill destroys the item therefore need to not touch the item when removing it here
    /// </summary>
    [Rpc(SendTo.Everyone)]
    public void DiscardItemRpc()
    {
        var item = AttachedItem;
        AttachmentSlot.Detach();
        OnRemoveItem.Invoke(item);
    }

    [Rpc(SendTo.Everyone)]
    public void DetachItemRpc() // Detaches item an makes it drop to the floor
    {
        var attachedItem = AttachedItem;
        DetachItem();
        if (attachedItem != null)
        {
            attachedItem.Drop();
        }
    }

    public void DetachItem()
    {
        if (AttachedItem == null)
            return;
        
        if (IsBackpackSlot && m_backpack.IsInInventory && m_backpack.IsOwner)
            AttachedItem.SetVisibility(true);

        AttachedItem.IsInteractable = true;
        AttachedItem.OnPickup -= OnPickup;
        if (AttachedItem.Corpse != null && AttachedItem.Corpse.Entity != null)
        {
            AttachedItem.Corpse.Entity.OnTakeDamageAction -= OnTakeDamageAction;
        }

        var item = AttachedItem;
        AttachedItem.DetachFromSlot();
        
        OnRemoveItem.Invoke(item);
    }

    public override void Interact(Entity entity)
    {
        if (entity.Inventory is not PlayerInventory pi)
            return;
        
        if (pi.CurrentItem != null && entity.IsOwner)
        {
            var item = pi.CurrentItem;
            if (IsItemAllowed(item))
            {
                pi.DropCurrentItem();
                if (item.TryGetComponent(out CorpseItem corpseItem))
                {
                    AttachItem(item, true);
                }
                else
                    AttachItem(item, true);
            }
        }
    }

    public override bool CanInteract(Entity entity)
    {
        if (entity is not EntityPlayer) return false; //  Only players allowed to interact with this
        if (HasAttachedItem) return false;
        if (entity.Inventory is not PlayerInventory pi)
            return false;
        if (!IsItemAllowed(pi.CurrentItem)) return false;
        if (DisableInteract) return false;

        if (IsBackpackSlot)
        {
            if (pi.CurrentItem != null)
            {
                if (pi.CurrentItem.IsBackpack) return false;
                if (entity.IsDead || entity.IsPickedUp || !IsSpawned) return false;
            }
        }

        if (pi.CurrentBigItem != null)
        {
            m_localSelectedItem = pi.CurrentBigItem;
        }
        else
        {
            m_localSelectedItem = pi.CurrentItem;
        }

        return true;
    }

    public void ReplaceItem(ulong itemNetworkObjectId)
    {
        bool found = NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(itemNetworkObjectId, out NetworkObject itemObj);
        if (!found)
        {
            Debug.LogError("[GetItem] Failed to find network object for Item!");
            return;
        }

        var item = itemObj.GetComponent<InventoryItem>();

        if (item == null)
            return;

        AttachItem(item, false);
    }

    bool IsItemAllowed(InventoryItem item)
    {
        if (item == null) return false;
        if (item.IsBackpack)
            return false;
        if (item.GetComponent<BucketUpgrade>() != null)
            return false;
        if (item.IsBig && !m_allowBigItems) return false;
        if (!item.IsBig && !m_allowSmallItems) return false;

        if (m_allowAnyItem) return true;
        if (m_allowedIDs.Contains(item.ItemID)) return true;
        if (m_allowedTypes.HasFlag(item.ItemType)) return true;
        
        return false;
    }
    
    void OnPickup(InventoryItem item)
    {
        if (IsBackpackSlot)
        {
            item.transform.localScale = m_defaultItemScale;
            item.ResetItemPriority();

            //if (m_backpack.IsInInventory && m_backpack.IsOwner)
            //    item.SetVisibility(true);
        }
        
        DetachItem();

        //OnRemoveItem.Invoke();
    }

    public override void SetSelection(bool selected)
    {
        base.SetSelection(selected);
        m_selected = selected;
    }

    protected virtual void OnPostRender()
    {
        if (!m_selected) return;
        if (m_localSelectedItem == null) return;
        
        if (m_drawMaterial == null) return;
        
        if (m_localSelectedItem.Corpse != null)
            m_localSelectedItem.Corpse.RefreshMeshInfo(AttachmentSlot.TargetGrip);
        
        m_drawMaterial.SetPass(0);

        var grip = m_localSelectedItem.GetGrip(AttachmentSlot);
        if (grip == null)
            return;

        Transform gripRoot = grip.GripRoot;
        var gripRootPos = gripRoot.transform.position;
        var gripRoootRot = gripRoot.transform.rotation;
        grip.UpdateGrip(AttachmentSlot);

        var gripMatrix = gripRoot.localToWorldMatrix;
        
        
        // draw mesh at the origin
        foreach (var meshInfo in m_localSelectedItem.MeshInfo)
        {
            Graphics.DrawMeshNow(meshInfo.m_mesh, gripMatrix * meshInfo.m_transform);
        }
        
        gripRoot.transform.position = gripRootPos;
        gripRoot.transform.rotation = gripRoootRot;
        Physics.SyncTransforms();
    }

    private void RenderPipelineManager_endCameraRendering(ScriptableRenderContext context, Camera camera)
    {
        OnPostRender();
    }

    void OnEnable()
    {
        RenderPipelineManager.endCameraRendering += RenderPipelineManager_endCameraRendering;
    }

    void OnDisable()
    {
        RenderPipelineManager.endCameraRendering -= RenderPipelineManager_endCameraRendering;
    }
}
                    
                

BACKPACK

The backpack is a big item, except it has quite a few differences from most big items so it inherits from inventory item.
On the backpack, there are object place spots, which mostly works as normal, except it hides the placed items for player it's equipped on.

FLAGGED VALUES

Values such as object placed items, killed NPCs, or unlocked areas and upgrades are saved as flags.
Some flagged values are can be dynamically removed, while others are permanant until a game over.

                    
public event Action<string> OnFlagSet;
public event Action<string> OnFlagUnset;

private HashSet<string> m_flags = new();
public HashSet<string> Flags => m_flags;

private Dictionary<string, ItemData> m_objectFlagsPlaced = new();
public Dictionary<string, ItemData> ObjectFlagsPlaced => m_objectFlagsPlaced;


public void AddObjectFlagsPlaced(string flag, ItemData item)
{
    ObjectFlagsPlaced.TryAdd(flag, item);
}

public void RemoveObjectFlagsPlaced(string flag)
{
    ObjectFlagsPlaced.Remove(flag);
}

public void SetFlag(string flag)
{
    SetFlagRpc(flag);
}

[Rpc(SendTo.Server)]
private void SetFlagRpc(string flag)
{
    m_flags.Add(flag);
    OnFlagSet?.Invoke(flag);
}

public void UnsetFlag(string flag)
{
    UnsetFlagRpc(flag);
}

[Rpc(SendTo.Server)]
private void UnsetFlagRpc(string flag)
{
    m_flags.Remove(flag);
    OnFlagUnset?.Invoke(flag);
}

public bool HasFlag(string flag)
{
    return m_flags.Contains(flag);
}
                    
                

GAMEMODE GOBLIN

The GameMode Goblin class is a singleton that handles the state and progress of the game. Current values and savedata values are set, and reset here.
When working on features, especially the saving system and flagged values, I've added to and worked in this script.

                    
public class GameMode_Goblin : NetworkBehaviour
{
    private static GameMode_Goblin _instance;
    public static GameMode_Goblin Instance => _instance;

    public static SaveData SaveData;
    private bool m_isSavingGame = false;
    private bool m_isLoadingGame = false;
    private bool m_isDeletingSave = false;
    public bool IsSavingGame => m_isSavingGame;
    public bool IsLoadingGame => m_isLoadingGame;
    public bool IsDeletingGame => m_isDeletingSave;
    [SerializeField] private string m_savingGameText;
    [SerializeField] private float m_savingGameTextDuration = 2.5f;


    public event Action<string> OnFlagSet;
    public event Action<string> OnFlagUnset;

    public event Action<bool> OnIsSavingGame;
    public event Action<bool> OnIsLoadingGame;
    public event Action<bool> OnIsDeletingGame;
    
    public event Action<bool, int, float> OnFeedingEvent;

    public bool HasLoadedSaveData => SaveData != null;
    public event Action OnSaveDataLoaded;

    private NetworkVariable<bool> m_gameOver = new(false);
    public bool GameOver => m_gameOver.Value;

    public enum LobbyState
    {
        Normal,
        Feeding,
    }

    public enum SafeArea
    {
        Lobby = 0,
        InBetween = 1,
        DarkBurg = 2
    }
    private SafeArea m_safeArea = new();
    public SafeArea CurrentSafeArea => m_safeArea;

    [Serializable]
    public struct FeedingSequenceData
    {
        public int m_daysBeforeFeeding;
        public int m_minimumFoodValue;
        public float m_2PlayersOrLessFactor;
    }


    [SerializeField] private FeedingSequenceData[] m_feedingSequenceData;
    
    private int m_maxNumCogs = 3;
    public int MaxNumCogs => m_maxNumCogs;


    private HashSet<string> m_flags = new();
    public HashSet<string> Flags => m_flags;

    private Dictionary<string, ItemData> m_objectFlagsPlaced = new();
    public Dictionary<string, ItemData> ObjectFlagsPlaced => m_objectFlagsPlaced;


    private NetworkVariable<LobbyState> m_lobbyState = new();

    private NetworkVariable<int> m_currentNumCogs = new();

    private NetworkVariable<int> m_currentDay = new();
    private NetworkVariable<int> m_currentLevel = new();
    private NetworkVariable<int> m_timeLeftInDungeon = new();
    private NetworkVariable<int> m_feedingTimes = new();

    private NetworkVariable<int> m_currentDepth = new();

    private NetworkVariable<int> m_totalFoodOffered = new();
    
    private NetworkVariable<int> m_foodValueToGetReward = new();
    private NetworkVariable<int> m_currentFoodValue = new();
    private NetworkVariable<bool> m_foodValueReached = new();
    private NetworkVariable<bool> m_hasReceivedReward = new();

    private NetworkVariable<bool> m_hubUnlocked = new();

    private StorageData m_bucketStorage = new StorageData() { Entities = new List<EntityData>(), Items = new List<ItemData>() };
    private StorageData m_itemStorage = new StorageData() { Entities = new List<EntityData>(), Items = new List<ItemData>() };
    private StorageData m_playerInventory = new StorageData() { Entities = new List<EntityData>(), Items = new List<ItemData>() };
    private NetworkVariable<bool> m_registerStorage = new();


    private NetworkVariable<int> m_seed = new();
    private List<bool> m_visitedDungeons = new();

    public void InvokeFeedingEvent(bool success, int bonusStage, float stageProgress)
    {
        OnFeedingEvent?.Invoke(success, bonusStage, stageProgress);
    }
    
    public void AddObjectFlagsPlaced(string flag, ItemData item)
    {
        ObjectFlagsPlaced.TryAdd(flag, item);
    }

    public void RemoveObjectFlagsPlaced(string flag)
    {
        ObjectFlagsPlaced.Remove(flag);
    }

    public void SetFlag(string flag)
    {
        SetFlagRpc(flag);
    }

    [Rpc(SendTo.Server)]
    private void SetFlagRpc(string flag)
    {
        m_flags.Add(flag);
        OnFlagSet?.Invoke(flag);
    }

    public void UnsetFlag(string flag)
    {
        UnsetFlagRpc(flag);
    }
    
    [Rpc(SendTo.Server)]
    private void UnsetFlagRpc(string flag)
    {
        m_flags.Remove(flag);
        OnFlagUnset?.Invoke(flag);
    }

    public bool HasFlag(string flag)
    {
        return m_flags.Contains(flag);
    }

    public int CurrentLevel => m_currentLevel.Value;
    public int CurrentDay => m_currentDay.Value;
    public int FeedingTimes => m_feedingTimes.Value;
    public int TotalFoodOffered => m_totalFoodOffered.Value;

    public NetworkVariable<LobbyState> CurrentLobbyState => m_lobbyState;
    public bool IsFeeding => m_lobbyState.Value == LobbyState.Feeding;

    public NetworkVariable<int> CurrentNumCogs => m_currentNumCogs;
    public NetworkVariable<int> TimeLeftInDungeon => m_timeLeftInDungeon;

    public NetworkVariable<int> CurrentDepth => m_currentDepth;

    public NetworkVariable<int> FoodValueToGetReward => m_foodValueToGetReward;
    public NetworkVariable<int> CurrentFoodValue => m_currentFoodValue;
    public NetworkVariable<bool> FoodValueReached => m_foodValueReached;
    public NetworkVariable<bool> HasReceivedReward => m_hasReceivedReward;

    public NetworkVariable<bool> HubUnlocked => m_hubUnlocked;

    public StorageData PlayerInventory => m_playerInventory;
    public StorageData BucketStorage => m_bucketStorage;
    public StorageData ItemStorage => m_itemStorage;
    public NetworkVariable<bool> RegisterStorage
    {
        get => m_registerStorage;
        set
        {
            if (!IsServer) 
                return;
            
            m_registerStorage = value;
        }
    }

    public NetworkVariable<int> Seed => m_seed;
    private IReadOnlyList<bool> VisitedDungeons => m_visitedDungeons;


    public void AddToStorage(ItemData data, StorageVessel storage)
    {
        switch (storage.Storage)
        {
            case StorageVessel.StorageType.ItemStorage: m_itemStorage.Add(data); break;
            case StorageVessel.StorageType.BucketStorage: m_bucketStorage.Add(data); break;
            case StorageVessel.StorageType.PlayerInventory: m_playerInventory.Add(data); break;
        }
    }
    
    public void AddToStorage(EntityData data, StorageVessel storage)
    {
        switch (storage.Storage)
        {
            case StorageVessel.StorageType.ItemStorage: m_itemStorage.Add(data); break;
            case StorageVessel.StorageType.BucketStorage: m_bucketStorage.Add(data); break;
            case StorageVessel.StorageType.PlayerInventory: m_playerInventory.Add(data); break;
        }
    }

    public void ClearStorage(StorageVessel storage)
    {
        switch (storage.Storage)
        {
            case StorageVessel.StorageType.ItemStorage: m_itemStorage.Clear(); break;
            case StorageVessel.StorageType.BucketStorage: m_bucketStorage.Clear(); break;
            case StorageVessel.StorageType.PlayerInventory: m_playerInventory.Clear(); break;
        }
    }

    public void SetStorage(StorageData storage, StorageVessel.StorageType storageType)
    {
        switch (storageType)
        {
            case StorageVessel.StorageType.ItemStorage: m_itemStorage = storage; break;
            case StorageVessel.StorageType.BucketStorage: m_bucketStorage = storage; break;
            case StorageVessel.StorageType.PlayerInventory: m_playerInventory = storage; break;
        }
    }

    public StorageData GetStorage(StorageVessel storage)
    {
        switch (storage.Storage)
        {
            case StorageVessel.StorageType.ItemStorage: return m_itemStorage;
            case StorageVessel.StorageType.BucketStorage: return m_bucketStorage;
            case StorageVessel.StorageType.PlayerInventory: return m_playerInventory;
        }
        Debug.LogError("Storage Type NOT FOUND!");
        return default;
    }


    public void ClearVisitedDungeons()
    {
        m_visitedDungeons.Clear();
    }
    public void AddVisitedDungeon(bool state)
    {
        m_visitedDungeons.Add(state);
    }
    public void SetVisitedDungeons(List<bool> visitedDungeons)
    {
        m_visitedDungeons = visitedDungeons;
    }
    public List<bool> GetVisitedDungeons()
    {
        return m_visitedDungeons;
    }
    public bool GetVisitedDungeonIndex(int index)
    {
        return m_visitedDungeons[index];
    }


    public FeedingSequenceData GetCurrentFeedingData()
    {
        int level = Mathf.Clamp(m_currentLevel.Value, 0, m_feedingSequenceData.Length - 1);
        return m_feedingSequenceData[level];
    }

    public FeedingSequenceData GetFeedingSequenceData(int level)
    {
        level = Mathf.Clamp(level, 0, m_feedingSequenceData.Length - 1);
        return m_feedingSequenceData[level];
    }
    

    private void Awake()
    {
        if (_instance != null)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;

        StartCoroutine(Wait());
    }

    private IEnumerator Wait()
    {
        yield return new WaitWhile(() => NetworkHandler.Singleton.IsLoadingScene);

        InitializeSaveData();

        if (ServiceLocator.GetService<GameStateService>().Current.State != GameStateAsset.GameState.DebugGame)
        {
            if (SaveLoad.IsCurrentSaveFileValid()) LoadGame();
            else RF_ManagersHolder.Instance.ChangeToNewSeed();
        }
        else 
        {
            RF_ManagersHolder.Instance.ChangeToNewSeed();
        }
        

        InitializeGameSeed();
    }

    private void InitializeGameSeed()
    {
        if (!IsServer) return;

        if (SaveLoad.IsCurrentSaveFileValid())
        {
            m_seed.Value = SaveData.Seed;
            Debug.Log($"Existing Seed Set! -> {m_seed.Value}");
        }
        else
        {
            m_seed.Value = Guid.NewGuid().GetHashCode();
            Debug.Log($"New Seed Generated! -> {m_seed.Value}");
        }
    }
    public void GenerateGameSeed()
    {
        if (!IsServer) return;

        m_seed.Value = Guid.NewGuid().GetHashCode();
        Debug.Log($"New Seed Generated! -> {m_seed.Value}");
    }


    public void IncreaseFeedingTimes()
    {
        if (!IsServer) return;       
        // todo: is m_feedingTimes the same as m_currentLevel? Should they be merged?
        m_feedingTimes.Value += 1;
        
        m_currentLevel.Value += 1;
        m_currentDay.Value = 0;
        //m_lobbyState.Value = LobbyState.Normal;
        
        SetTimeLeftInDungeon();
    }

    public void IncreaseFoodOffered(float food)
    {
        if (!IsServer) return;
        m_totalFoodOffered.Value += (int)food;
    }
    public void SetFoodOffered(int value)
    {
        if (!IsServer) return;
        m_totalFoodOffered.Value = value;
    }


    public void IncreaseDay()
    {
        var feedingData = GetCurrentFeedingData();

        // todo: unsure if this can be moved to increase feeding
        // it feels off handling level up here only when you try to go to a dungeon after a feeding session
        
        // First check if we have completed a level
        // if (m_currentDay.Value >= feedingData.m_daysBeforeFeeding)
        // {
        //     m_currentLevel.Value++;
        //     feedingData = GetCurrentFeedingData();
        //     m_currentDay.Value = 0;
        //     m_lobbyState.Value = LobbyState.Normal;
        //
        //     m_timeLeftInDungeon.Value = feedingData.m_daysBeforeFeeding;
        // }
        
        if (m_timeLeftInDungeon.Value == 0 && m_currentDay.Value == 0)
        {
            m_timeLeftInDungeon.Value = feedingData.m_daysBeforeFeeding;
        }

        // Go to next day in sequence
        m_currentDay.Value += 1;
        m_timeLeftInDungeon.Value -= 1;
        
        if (m_currentDay.Value >= feedingData.m_daysBeforeFeeding)
        {
            m_lobbyState.Value = LobbyState.Feeding;
        }
    }
/*
#if !SUBMISSION_BUILD
    public void DebugDungeonVisit_IncreaseDay()
    {
        var feedingData = GetCurrentFeedingData();
        
        if (m_timeLeftInDungeon.Value == 0 && m_currentDay.Value == 0)
            m_timeLeftInDungeon.Value = feedingData.m_daysBeforeFeeding;

        // Go to next day in sequence
        m_currentDay.Value = Mathf.Clamp(m_currentDay.Value + 1, 0, feedingData.m_daysBeforeFeeding);
        m_timeLeftInDungeon.Value = Mathf.Clamp(m_timeLeftInDungeon.Value - 1, 0, feedingData.m_daysBeforeFeeding);
        if (m_currentDay.Value >= feedingData.m_daysBeforeFeeding)
        {
            m_lobbyState.Value = LobbyState.Feeding;
        }
    }
#endif
*/
    public void SetCurrentDay(int value)
    {
        m_currentDay.Value = value;
    }

    public void SetSafeArea(SafeArea area)
    {
        m_safeArea = area;
    }
    
    public void SetTimeLeftInDungeon()
    {
        if (!IsServer) return;
        
        // int i = 0;
        // if (FeedingTimes > 0) i = 1;
        // else i = 0;
        // var timeLeft = GetFeedingSequenceData(CurrentLevel + i).m_daysBeforeFeeding;

        var timeLeft = GetFeedingSequenceData(CurrentLevel).m_daysBeforeFeeding;
        m_timeLeftInDungeon.Value = timeLeft;
    }

    public void UpdateNumCogs()
    {
        if (!IsServer) return;

        int cogs = 0;

        for (int i = 0; i < MaxNumCogs; ++i)
        {
            if (HasFlag($"cog_{i}"))
            {
                ++cogs;
            }
        }
        CurrentNumCogs.Value = cogs;
    }

    public void SetGameOver(bool state)
    {
        if (!IsServer)
            return;

        m_gameOver.Value = state;
    }

    public void ResetDay()
    {
        m_lobbyState.Value = LobbyState.Normal;

        m_safeArea = SafeArea.Lobby;

        m_currentLevel.Value = 0;
        m_currentDay.Value = 0;
        m_feedingTimes.Value = 0;
        m_totalFoodOffered.Value = 0;
        SetTimeLeftInDungeon();

        m_itemStorage.Clear();
        m_bucketStorage.Clear();
        m_playerInventory.Clear();
        
        m_currentNumCogs.Value = 0;

        m_objectFlagsPlaced.Clear();
        m_flags.Clear();

        m_foodValueReached.Value = false;
        m_hasReceivedReward.Value = false;
        m_currentFoodValue.Value = 0;

        m_hubUnlocked.Value = false;

        m_gameOver.Value = false;

        GenerateGameSeed();
        m_visitedDungeons.Clear();
        
        RF_ManagersHolder.Instance?.ChangeToNewSeed();
        DiscoveryManager.Instance?.ResetDiscoveriesRpc();

        if (ServiceLocator.GetService<GameStateService>().IsRandomGame == false)
            DeleteSaveGameData();
    }

    public void InitializeSaveData()
    {
        if (!IsServer) return;

        SaveData = new SaveData();
    }

    private void SaveObjectFlagsPlaced()
    {
        SaveData.ObjectFlagsPlaced_KEYS.Clear();
        SaveData.ObjectFlagsPlaced_VALUES.Clear();

        foreach (var kvp in m_objectFlagsPlaced)
        {
            SaveData.ObjectFlagsPlaced_KEYS.Add(kvp.Key);
            SaveData.ObjectFlagsPlaced_VALUES.Add(kvp.Value);
        }
    }

    private void LoadObjectFlagsPlaced(SaveData saveData)
    {
        m_objectFlagsPlaced.Clear();

        if (saveData.ObjectFlagsPlaced_KEYS.Count != saveData.ObjectFlagsPlaced_VALUES.Count)
        {
            Debug.LogError("KEYS AND VALUES NOT SYNCED!");
            return;
        }
        for (int i = 0; i < saveData.ObjectFlagsPlaced_KEYS.Count; ++i)
        {
            m_objectFlagsPlaced.TryAdd(saveData.ObjectFlagsPlaced_KEYS[i], saveData.ObjectFlagsPlaced_VALUES[i]);
        }
    }

    public void SetSaveData()
    {
        m_isSavingGame = true;
        OnIsSavingGame?.Invoke(true);

        SaveData saveData = SaveData;
        SaveLoad.Save(saveData);

        m_isSavingGame = false;
        OnIsSavingGame?.Invoke(false);
    }

    public void SendSavingGameMessage()
    {
        SendSavingGameMessageRpc();
    }

    [Rpc(SendTo.Everyone)]
    private void SendSavingGameMessageRpc()
    {
        UIPlayerHUD.Instance.InfoText.SendInfoText(m_savingGameText, m_savingGameTextDuration);
    }
    public void SaveGame()
    {
        if (!IsServer) return;

        SendSavingGameMessage();

        m_isSavingGame = true;
        OnIsSavingGame?.Invoke(true);

        RegisterStorage.Value = true;

        SaveObjectFlagsPlaced();

        SaveData.Flags.Clear();
        foreach (var flag in m_flags)
        {
            SaveData.Flags.Add(flag);
        }

        SaveData.CurrentNumCogs = CurrentNumCogs.Value;

        SaveData.CurrentLevel = CurrentLevel;
        SaveData.CurrentDay = CurrentDay;
        SaveData.TimeLeftInDungeon = TimeLeftInDungeon.Value;
        SaveData.FeedingTimes = FeedingTimes;
        SaveData.TotalFoodOffered = TotalFoodOffered;

        SaveData.FoodValueReached = FoodValueReached.Value;
        SaveData.HasReceivedReward = HasReceivedReward.Value;
        SaveData.CurrentFoodValue = CurrentFoodValue.Value;

        SaveData.HubUnlocked = HubUnlocked.Value;

        SaveData.ItemStorage = ItemStorage;
        SaveData.BucketStorage = BucketStorage;
        SaveData.PlayerInventory = PlayerInventory;

        SaveData.PlayerGold = ResourceManager.Instance.PlayerGold;
        SaveData.TotalGoldGathered = ResourceManager.Instance.TotalGoldGathered;

        SaveData.Seed = Seed.Value;
        SaveData.SessionSeed = RF_ManagersHolder.Instance.Seed;
        SaveData.VisitedDungeons = (List<bool>)VisitedDungeons;
        SaveData.DiscoveredItems = DiscoveryManager.Instance.HasDiscoveredArray.ToList();

        SaveData.CurrentDepth = BucketSpawner.Bucket.Depth.Value;

        SaveData.CurrentSafeArea = (int)CurrentSafeArea;

        SaveData saveData = SaveData;
        SaveLoad.Save(saveData);

        m_isSavingGame = false;
        OnIsSavingGame?.Invoke(false);
    }

    public void LoadGame()
    {
        if (!IsServer) return;

        SaveData saveData = SaveLoad.Load();
        if (saveData == null) return;

        m_isLoadingGame = true;
        OnIsLoadingGame?.Invoke(true);

        LoadObjectFlagsPlaced(saveData);

        m_flags.Clear();
        foreach (var flag in saveData.Flags)
        {
            SetFlag(flag);
        }

        CurrentNumCogs.Value = saveData.CurrentNumCogs;
        CurrentNumCogs.Value = Mathf.Clamp(CurrentNumCogs.Value, 0, 3);

        m_currentLevel.Value = saveData.CurrentLevel;
        m_currentDay.Value = saveData.CurrentDay;
        m_timeLeftInDungeon.Value = saveData.TimeLeftInDungeon;
        m_feedingTimes.Value = saveData.FeedingTimes;
        m_totalFoodOffered.Value = saveData.TotalFoodOffered;

        HasReceivedReward.Value = saveData.HasReceivedReward;
        FoodValueReached.Value = saveData.FoodValueReached;
        CurrentFoodValue.Value = saveData.CurrentFoodValue;

        HubUnlocked.Value = saveData.HubUnlocked;

        DiscoveryManager.Instance.LoadDiscoveredArrayRpc(saveData.DiscoveredItems.ToArray());

        SetStorage(saveData.ItemStorage, StorageVessel.StorageType.ItemStorage);
        SetStorage(saveData.BucketStorage,StorageVessel.StorageType.BucketStorage);
        SetStorage(saveData.PlayerInventory, StorageVessel.StorageType.PlayerInventory);

        ResourceManager.Instance.SetTotalGold(saveData.TotalGoldGathered);
        if (SaveLoad.IsCurrentSaveFileValid()) ResourceManager.Instance.SetGold(saveData.PlayerGold);

        m_seed.Value = saveData.Seed;
        RF_ManagersHolder.Instance.ChangeToNewSeed(saveData.SessionSeed);
        SetVisitedDungeons(saveData.VisitedDungeons);

        CurrentDepth.Value = saveData.CurrentDepth;

        SetSafeArea((SafeArea)saveData.CurrentSafeArea);

        SaveData = saveData;

        m_isLoadingGame = false;
        OnIsLoadingGame?.Invoke(false);
        
        OnSaveDataLoaded?.Invoke();
    }

    public void DeleteSaveGameData()
    {
        if (!IsServer) return;

        m_isDeletingSave = true;
        OnIsDeletingGame?.Invoke(true);

        SaveLoad.Delete();

        m_isDeletingSave = false;
        OnIsDeletingGame?.Invoke(false);
    }
}