using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Mirror;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.Serialization;
namespace PrefsGUI.Sync
{
///
/// Sync PrefsGUI parameter over UNET
///
[DefaultExecutionOrder(-1)]
public class PrefsGUISyncForMirror : NetworkBehaviourDivideSpawnData, ISerializationCallbackReceiver
{
[Tooltip("Prefs that have never been changed from their default values will not be synced.")]
public bool syncOnlyValueChangedPrefs = true;
// want use HashSet but use List so it will be serialized on Inspector
[SerializeField]
[FormerlySerializedAs("ignoreKeys")]
private List ignoreKeyList = new();
private HashSet _ignoreKeys;
private Dictionary _prefsAndCallbackTable;
private readonly HashSet _sendPrefsSet = new();
private readonly List _newPrefsList = new();
private HashSet _receivedKeys;
private readonly SyncDictionary _syncDictionary = new();
private List _divideSpawnDataSyncObjects;
public IEnumerable IgnoreKeys => ignoreKeyList;
protected override IEnumerable DivideTargetSyncObjects =>
_divideSpawnDataSyncObjects ??= new List { _syncDictionary };
#region Unity
public void Awake()
{
syncInterval = 0f;
}
public void Update()
{
SendPrefs();
if (isClientOnly)
{
ReadPrefs();
}
}
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
_ignoreKeys = new(ignoreKeyList);
}
#endregion
#region Mirror
public override void OnStartServer()
{
base.OnStartServer();
RegisterPrefsCallbacks();
}
public override void OnStopServer()
{
base.OnStopServer();
ClearPrefsCallbacks();
}
public override void OnStartClient()
{
// base.OnStartClient()内でonSpawnFinishedが呼ばれることがあるためメソッドの順番注意
if (isClientOnly)
{
onSpawnFinished.AddListener(OnSpawnFinished);
}
base.OnStartClient();
}
#endregion
#region IgnoreKey
public bool HasIgnoreKey(string key) => _ignoreKeys.Contains(key);
public void AddIgnoreKey(string key)
{
ignoreKeyList.Add(key);
_ignoreKeys.Add(key);
}
public void RemoveIgnoreKey(string key)
{
ignoreKeyList.RemoveAll(k => k == key);
_ignoreKeys.Remove(key);
}
#endregion
#region Server
private void RegisterPrefsCallbacks()
{
var existingPrefsEnumerable = PrefsParam.all.Where(prefs => !_ignoreKeys.Contains(prefs.key));
foreach (var prefs in existingPrefsEnumerable)
{
RegisterPrefsValueChangedCallback(prefs);
}
PrefsParam.onRegisterPrefsParam += OnRegisterPrefsParam;
}
private void RegisterPrefsValueChangedCallback(PrefsParam prefs)
{
_prefsAndCallbackTable ??= new Dictionary();
var success = _prefsAndCallbackTable.TryAdd(prefs, ValueChangedCallback);
if (!success) return;
prefs.RegisterValueChangedCallback(ValueChangedCallback);
// ここで prefs.IsDefault はエラーになる
// PrefsParam.onRegisterPrefsParam 経由で呼ばれるとAwake()内等などで、prefsのデフォルト値へのアクセスがエラーになるっぽい
//
// var needSend = !prefs.IsDefault || !syncOnlyValueChangedPrefs;
// if (needSend)
// {
// ValueChangedCallback();
// }
_newPrefsList.Add(prefs);
return;
void ValueChangedCallback()
{
_sendPrefsSet.Add(prefs);
}
}
private void OnRegisterPrefsParam(PrefsParam prefs)
{
if (_ignoreKeys.Contains(prefs.key)) return;
RegisterPrefsValueChangedCallback(prefs);
}
private void ClearPrefsCallbacks()
{
if (_prefsAndCallbackTable != null)
{
foreach (var (prefs, callback) in _prefsAndCallbackTable)
{
prefs.UnregisterValueChangedCallback(callback);
}
_prefsAndCallbackTable.Clear();
}
PrefsParam.onRegisterPrefsParam -= OnRegisterPrefsParam;
}
[ServerCallback]
private void SendPrefs()
{
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var prefs in _newPrefsList)
{
var needSend = !syncOnlyValueChangedPrefs || !prefs.IsDefault;
if (!needSend) continue;
_sendPrefsSet.Add(prefs);
}
_newPrefsList.Clear();
foreach (var prefs in _sendPrefsSet)
{
WritePrefsToSyncDictionary(prefs);
}
_sendPrefsSet.Clear();
}
private readonly Dictionary> _toDictionaryTable = new();
private static readonly MethodInfo CreateToDictionaryMethodInfo =
typeof(PrefsGUISyncForMirror).GetMethod(nameof(CreateToDictionaryAction),
BindingFlags.Instance | BindingFlags.NonPublic);
private void WritePrefsToSyncDictionary(PrefsParam prefs)
{
var innerType = prefs.GetInnerType();
if (!_toDictionaryTable.TryGetValue(innerType, out var action))
{
action = (Action)CreateToDictionaryMethodInfo.MakeGenericMethod(innerType).Invoke(this, null);
_toDictionaryTable[innerType] = action;
}
action(prefs);
}
private Action CreateToDictionaryAction()
{
var toByteDictionary = new PrefsToByteDictionary(_syncDictionary);
return (prefs) => toByteDictionary.WriteFrom(prefs.GetInnerAccessor());
}
#endregion
#region Client
private void OnSpawnFinished()
{
_receivedKeys = new HashSet(_syncDictionary.Keys);
// If you get an error with Mirror 89.*, please update to 89.4 or later.
// No way to know minor version in code.
// - https://github.com/MirrorNetworking/Mirror/releases/tag/v89.4.0
#if MIRROR_89_OR_NEWER
_syncDictionary.OnAdd += (key) => _receivedKeys.Add(key);
_syncDictionary.OnSet += (key, _) => _receivedKeys.Add(key);
_syncDictionary.OnRemove += (key, _) => _receivedKeys.Remove(key);
_syncDictionary.OnClear += () => _receivedKeys.Clear();
#else
_syncDictionary.Callback += (op, key, _) =>
{
switch (op)
{
case SyncIDictionary.Operation.OP_ADD:
case SyncIDictionary.Operation.OP_SET:
_receivedKeys.Add(key);
break;
case SyncIDictionary.Operation.OP_CLEAR:
case SyncIDictionary.Operation.OP_REMOVE:
_receivedKeys.Remove(key);
break;
}
};
#endif
ReadPrefs(true);
}
[ClientCallback]
private void ReadPrefs(bool checkAlreadyGet = false)
{
if ( _receivedKeys == null) return;
var allDic = PrefsParam.allDic;
using var _ = ListPool.Get(out var removeKeys);
foreach (var key in _receivedKeys)
{
if (!allDic.TryGetValue(key, out var prefs)) continue;
removeKeys.Add(key);
var bytes = _syncDictionary[key];
var valueChangedAndAlreadyGet = WriteBytesToPrefs(bytes, prefs);
if (checkAlreadyGet && valueChangedAndAlreadyGet)
{
Debug.LogWarning($"key:[{prefs.key}] Get() called before it was rewritten in sync. The application may be using pre-sync values, consider using PrefsGUISync.IsSpawnFinished or onSpawnFinished event.");
}
}
_receivedKeys.ExceptWith(removeKeys);
}
private readonly Dictionary _setToPrefsFuncTable = new();
private readonly MethodInfo _createToPrefsActionMethodInfo =
typeof(PrefsGUISyncForMirror).GetMethod(nameof(CreateSetToPrefsFunc),
BindingFlags.NonPublic | BindingFlags.Static);
private bool WriteBytesToPrefs(byte[] bytes, PrefsParam prefs)
{
var innerType = prefs.GetInnerType();
if (!_setToPrefsFuncTable.TryGetValue(innerType, out var func))
{
func = (SetToPrefsFunc)_createToPrefsActionMethodInfo.MakeGenericMethod(innerType).Invoke(null, null);
_setToPrefsFuncTable[innerType] = func;
}
return func(bytes, prefs);
}
// return Already Get()'d and value update
private delegate bool SetToPrefsFunc(byte[] bytes, PrefsParam prefs);
private static SetToPrefsFunc CreateSetToPrefsFunc()
{
return (bytes, prefs) =>
{
var value = BytesConverter.BytesToValue(bytes);
var innerAccessor = prefs.GetInnerAccessor();
var alreadyGet = innerAccessor.IsAlreadyGet;
var valueUpdated = innerAccessor.SetSyncedValue(value);
return alreadyGet && valueUpdated;
};
}
#endregion
}
}