Untitled - CSHARP 10.82 KB
                                
                                    using Amnesia.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;

namespace Amnesia.Data
{
    internal class PlayerRecord
    {
        #region constants
        private const string ROOT = "amnesiaPlayerRecord";
        private const string USER_IDENTIFIER = "userIdentifier";
        private const string ENTITY_ID = "entityId";
        private const string CHANGES = "changes";
        private const string CHANGE = "change";
        private const string NAME = "name";
        private const string LEVEL = "level";
        private const string FILENAME_EXTENSION = "apr";
        #endregion

        private static readonly ModLog<PlayerRecord> _log = new ModLog<PlayerRecord>();

        public static Dictionary<int, PlayerRecord> Entries { get; private set; } = new Dictionary<int, PlayerRecord>();

        public int EntityId { get; private set; }
        public PlatformUserIdentifierAbs UserIdentifier { get; private set; }
        public int Level { get; private set; } = 0;
        public List<(string, int)> Changes { get; private set; } = new List<(string, int)>();

        public static void Load(ClientInfo clientInfo)
        {
            var entityId = ClientInfoHelper.SafelyGetEntityIdFor(clientInfo);
            var userIdentifier = ClientInfoHelper.GetUserIdentifier(clientInfo);
            if (Entries.ContainsKey(entityId))
            {
                _log.Error($"Player Record is already loaded for player {entityId} / {userIdentifier.CombinedString} and this is NOT expected.");
                return;
            }
            if (!GameManager.Instance.World.Players.dict.TryGetValue(entityId, out var player))
            {
                _log.Error($"Could not find player at {entityId} / {userIdentifier.CombinedString} even though one is logging in with this info.");
                return;
            }
            var playerRecord = new PlayerRecord(entityId, userIdentifier, player.Progression.Level, player.Progression.SkillPoints);
            var filename = Path.Combine(GameIO.GetPlayerDataDir(), $"{userIdentifier}.apr");
            try
            {
                var xml = new XmlDocument();
                xml.Load(filename);
                var changes = xml.GetElementsByTagName(CHANGE);
                for (var i = 0; i < changes.Count; i++)
                {
                    var name = changes[i].Attributes[NAME].Value;
                    var level = int.Parse(changes[i].Attributes[LEVEL].Value);
                    playerRecord.Changes.Add((name, level));
                }
                _log.Info($"Successfully loaded {filename}");
            }
            catch (FileNotFoundException)
            {
                _log.Info($"No player record file found for player {entityId}; creating a new one with defaults under {filename}");
                playerRecord.Save();
            }
            catch (Exception e)
            {
                _log.Error($"Failed to load player record file {filename}; attempting to recover from backup.", e);

                // TODO: try to recover from backup

                // TODO: if backup recovery failed, store broken file under different filename for future reference
                var failureFilename = filename + ".failure";

                // otherwise, create default
                _log.Info($"Unable to recover player record for player {entityId}; creating a new one with defaults under {filename}; admin can attempt to inspect backup file {failureFilename}");
                playerRecord.Save();
            }
            finally
            {
                Entries.Add(entityId, playerRecord);
            }
        }

        public static void Unload(ClientInfo clientInfo)
        {
            var entityId = ClientInfoHelper.SafelyGetEntityIdFor(clientInfo);
            if (Entries.TryGetValue(entityId, out var playerRecord))
            {
                //playerRecord.Save(); // TODO: save as backup instead?
                playerRecord.Changes.Clear(); // proactively free memory
                _ = Entries.Remove(entityId);
            }
        }

        public PlayerRecord(int entityId, PlatformUserIdentifierAbs userIdentifier, int level, int unspentSkillPoints)
        {
            EntityId = entityId;
            UserIdentifier = userIdentifier;
            Level = level;
        }

        public void Save()
        {
            var filename = Path.Combine(GameIO.GetPlayerDataDir(), $"{UserIdentifier}.{FILENAME_EXTENSION}");
            try
            {
                var xml = new XmlDocument();
                var root = xml.AddXmlElement(ROOT);
                root.AddXmlElement(ENTITY_ID).InnerText = EntityId.ToString();
                root.AddXmlElement(USER_IDENTIFIER).InnerText = UserIdentifier.CombinedString;
                var changes = root.AddXmlElement(CHANGES);
                for (var i = 0; i < Changes.Count; i++)
                {
                    var change = changes.AddXmlElement(CHANGE);
                    change.SetAttribute(NAME, Changes[i].Item1);
                    change.SetAttribute(LEVEL, Changes[i].Item2.ToString());
                }
                xml.Save(filename);
                _log.Trace($"Successfully saved {filename}");
                // TODO: perhaps also save up to 1 backup?
            }
            catch (Exception e)
            {
                _log.Error($"Failed to save Player Record for {EntityId}", e);
            }
        }

        /// <summary>
        /// Set player level and automatically infer the change to unspent skill points.
        /// </summary>
        /// <param name="level">Level to set this record to.</param>
        public void UpdateLevel(int level)
        {
            if (Level != level)
            {
                _log.Trace($"Player {EntityId}'s level changed: {Level} -> {level}");
                Level = level;
            }
        }

        /// <summary>
        /// Set player level without inferring any change to unspent skill points.
        /// </summary>
        /// <param name="level">Level to set this record to.</param>
        public void SetLevel(int level)
        {
            if (Level != level)
            {
                _log.Trace($"Player {EntityId}'s level changed: {Level} -> {level}");
                Level = level;
            }
        }

        /// <summary>
        /// Record skill acquisition and incorporate cost into unspent skill points pool.
        /// </summary>
        /// <param name="name">name of skill</param>
        /// <param name="level">skill level acquired</param>
        /// <param name="cost">cost in skill points for this skill</param>
        public void PurchaseSkill(string name, int level, int cost)
        {
            _log.Trace($"Player {EntityId} purchased {name} at level {level} for {cost} skill {(cost == 1 ? "point" : "points")}");
            Changes.Add((name, level));
            Save();
        }

        /// <summary>
        /// Respec player, returning/unassigning all skill points but leaving level the same.
        /// </summary>
        /// <param name="player">Player to respec.</param>
        public void Respec(ClientInfo clientInfo, EntityPlayer player)
        {
            player.Progression.ResetProgression(true);
            Changes.Clear();
            Save();
            player.Progression.bProgressionStatsChanged = true;
            player.bPlayerStatsChanged = true;
            ConnectionManager.Instance.SendPackage(NetPackageManager.GetPackage<NetPackagePlayerStats>().Setup(player), false, player.entityId);

        }

        /// <summary>
        /// Apply as many recorded skills in order as can be done; this is meant to be called after resetting player levels and skill points.
        /// </summary>
        /// <remarks>Logic lifted in part from XUiC_SkillPerkLevel.btnBuy_OnPress. <br />Should only call this method immediately after receiving a sync update for EntityPlayer's progression.</remarks>
        /// <param name="player">Reliable EntityPlayer that can be trusted</param>
        public void ReapplySkills(EntityPlayer player)
        {
            ValidateAndRepairChangeIntegrity(player);

            int i;
            for (i = 0; i < Changes.Count; i++)
            {
                var progressionValue = player.Progression.GetProgressionValue(Changes[i].Item1);
                var cost = progressionValue.ProgressionClass.CalculatedCostForLevel(Changes[i].Item2);
                if (cost > player.Progression.SkillPoints)
                {
                    break;
                }

                player.Progression.SkillPoints -= cost;
                progressionValue.Level = Changes[i].Item2;
            }

            Changes = Changes.GetRange(0, i); // forget remaining entries we couldn't afford
        }

        /// <summary>
        /// Attempt to repair any missing skills that may've been lost due to bugs/issues.
        /// </summary>
        /// <param name="dummy">entity for use in acquiring progression value only (TODO: possibly replace in the future)</param>
        public void ValidateAndRepairChangeIntegrity(EntityPlayer dummy)
        {
            var list = new List<(string, int)>();
            var dict = new Dictionary<string, int>();
            for (var i = 0; i < Changes.Count; i++)
            {
                var name = Changes[i].Item1;
                var level = Changes[i].Item2;

                if (!dict.ContainsKey(name))
                {
                    dict.Add(name, dummy.Progression.GetProgressionValue(Changes[i].Item1).ProgressionClass.MinLevel);
                }

                // fill any gaps that may've been missed
                while (dict[name] < level)
                {
                    dict[name]++;
                    list.Add((name, dict[name]));
                }
            }
            Changes = list;
            Save();
        }
    }
}
                                
                            

Paste Hosted With By Wklejamy.pl