using System;
using System.Collections.ObjectModel;
using System.Threading;
using ACE.Common;
using ACE.Database;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Managers;
using ACE.Server.Network.GameMessages.Messages;
namespace ACE.Server.WorldObjects
{
partial class Player
{
public const long DefaultPlayerSaveIntervalSecs = 300; // default to 5 minutes
public DateTime CharacterLastRequestedDatabaseSave { get; protected set; }
///
/// This variable is set to true when a change is made, and set to false before a save is requested.
/// The primary use for this is to trigger save on add/modify/remove of properties.
///
public bool CharacterChangesDetected { get; set; }
///
/// Set to true when SaveCharacter() returns a failure
///
public bool CharacterSaveFailed { get; set; }
///
/// Set to true when SaveBiotaToDatabase() returns a failure
///
public bool BiotaSaveFailed { get; set; }
///
/// The time period between automatic saving of player character changes
///
public long PlayerSaveIntervalSecs
{
get => PropertyManager.GetLong("player_save_interval", DefaultPlayerSaveIntervalSecs).Item;
}
///
/// Best practice says you should use this lock any time you read/write the Character.
///
/// For absolute maximum performance, if you're willing to assume (and risk) the following:
/// - that the character in the database will not be modified (in a way that adds or removes properties) outside of ACE while ACE is running with a reference to that character
/// - that the character will only be read/modified by a single thread in ACE
/// You can remove the lock usage for any Get/GetAll Property functions. You would simply use it for Set/Remove Property functions because each of these could end up adding/removing to the collections.
/// The critical thing is that the collections are not added to or removed from while Entity Framework is iterating over them.
/// Mag-nus 2018-08-19
///
public readonly ReaderWriterLockSlim CharacterDatabaseLock = new ReaderWriterLockSlim();
private void SetPropertiesAtLogOut()
{
LogoffTimestamp = Time.GetUnixTime();
// These properties are used with offline players to determine passup rates
SetProperty(PropertyInt.CurrentLoyaltyAtLastLogoff, (int)GetCreatureSkill(Skill.Loyalty).Current);
SetProperty(PropertyInt.CurrentLeadershipAtLastLogoff, (int)GetCreatureSkill(Skill.Leadership).Current);
}
///
/// This will make sure a player save happens no later than the current time + seconds
///
public void RushNextPlayerSave(int seconds)
{
if (LastRequestedDatabaseSave.AddSeconds(PlayerSaveIntervalSecs) <= DateTime.UtcNow.AddSeconds(seconds))
return;
LastRequestedDatabaseSave = DateTime.UtcNow.AddSeconds(seconds).AddSeconds(-1 * PlayerSaveIntervalSecs);
}
///
/// Saves the character to the persistent database. Includes Stats, Position, Skills, etc.
/// Will also save any possessions that are marked with ChangesDetected.
///
public void SavePlayerToDatabase()
{
if (CharacterChangesDetected)
SaveCharacterToDatabase();
var biotas = new Collection<(Biota biota, ReaderWriterLockSlim rwLock)>();
SaveBiotaToDatabase(false);
biotas.Add((Biota, BiotaDatabaseLock));
var allPossession = GetAllPossessions();
foreach (var possession in allPossession)
{
if (possession.ChangesDetected)
{
possession.SaveBiotaToDatabase(false);
biotas.Add((possession.Biota, possession.BiotaDatabaseLock));
}
}
var requestedTime = DateTime.UtcNow;
DatabaseManager.Shard.SaveBiotasInParallel(biotas, result =>
{
log.DebugFormat("{0} has been saved. It took {1:N0} ms to process the request.", Name, (DateTime.UtcNow - requestedTime).TotalMilliseconds);
if (!result)
{
// This will trigger a boot on next player tick
BiotaSaveFailed = true;
}
});
}
public void SaveCharacterToDatabase()
{
CharacterLastRequestedDatabaseSave = DateTime.UtcNow;
CharacterChangesDetected = false;
//DatabaseManager.Shard.SaveCharacter(Character, CharacterDatabaseLock, null);
DatabaseManager.Shard.SaveCharacter(Character, CharacterDatabaseLock, result =>
{
if (!result)
{
if (this is Player player)
{
// This will trigger a boot on next player tick
CharacterSaveFailed = true;
}
}
});
}
}
}