using KFCommonUtilityLib.Scripts.ConsoleCmd; using KFCommonUtilityLib.Scripts.Utilities; using System; using System.Collections.Generic; using UniLinq; using UnityEngine; namespace KFCommonUtilityLib.Scripts.StaticManagers { //concept: maintain an entityID-AltActionIndice mapping on both server and client //and get the correct action before calling ItemAction.* //always set MinEventParams.itemActionData //done: set meta and ammoindex on switching mode, keep current mode in metadata //should take care of accuracy updating //partially done: should support shared meta //alt actions should be considered primary, redirect index == 0 to custom method //redirect ItemClass.Actions[0] to custom method //however, player input handling is redirected to action0 so that alternative module can dispatch it to correct action. //patch GameManager.updateSendClientPlayerPositionToServer to sync data, so that mode change always happens after holding item change public struct MultiActionIndice { public const int MAX_ACTION_COUNT = 3; public unsafe fixed sbyte indices[MAX_ACTION_COUNT]; public unsafe fixed sbyte metaIndice[MAX_ACTION_COUNT]; public readonly byte modeCount; public unsafe MultiActionIndice(ItemClass item) { ItemAction[] actions = item.Actions; indices[0] = 0; metaIndice[0] = 0; byte last = 1; for (sbyte i = 3; i < actions.Length && last < MAX_ACTION_COUNT; i++) { if (actions[i] != null) { indices[last] = i; if (actions[i].Properties.Values.TryGetValue("ShareMetaWith", out string str) && sbyte.TryParse(str, out sbyte shareWith)) { metaIndice[last] = shareWith; } else { metaIndice[last] = i; } last++; } } modeCount = last; for (; last < MAX_ACTION_COUNT; last++) { indices[last] = -1; metaIndice[last] = -1; } } public unsafe int GetActionIndexForMode(int mode) { return indices[mode]; } public unsafe int GetMetaIndexForMode(int mode) { return metaIndice[mode]; } public unsafe int GetMetaIndexForActionIndex(int actionIndex) { return metaIndice[GetModeForAction(actionIndex)]; } public int GetModeForAction(int actionIndex) { int mode = -1; for (int i = 0; i < MultiActionIndice.MAX_ACTION_COUNT; i++) { unsafe { if (indices[i] == actionIndex) { mode = i; break; } } } return mode; } } //MultiActionMapping instance should be changed on ItemAction.StartHolding, so we only need to send curIndex. public class MultiActionMapping { public const string STR_MULTI_ACTION_INDEX = "MultiActionIndex"; public readonly MultiActionIndice indices; private int slotIndex; private int curIndex; private int lastDisplayMode = -1; private readonly bool[] unlocked; private ActionModuleAlternative.AlternativeData altData; public EntityAlive entity; public string toggleSound; public ItemValue ItemValue { get { var res = entity.inventory.GetItem(slotIndex).itemValue; if (res.IsEmpty()) { return null; } return res; } } public int SlotIndex => slotIndex; /// /// when set CurIndex from local input, also set manager to dirty to update the index on other clients /// public int CurMode { get => curIndex; set { unsafe { if (value < 0) value = 0; else { while (value < MultiActionIndice.MAX_ACTION_COUNT) { if (unlocked[value]) break; value++; } //mostly for CurIndex++, cycle through available indices if (value >= MultiActionIndice.MAX_ACTION_COUNT || indices.indices[value] == -1) value = 0; } if (curIndex == value) return; SaveMeta(); //load current meta and ammo index from metadata curIndex = value; ReadMeta(); entity.emodel?.avatarController?.UpdateInt(MultiActionUtils.ExecutingActionIndexHash, CurActionIndex, false); //altData.OverrideMuzzleTransform(curIndex); } } } //for ItemClass.Actions access public int CurActionIndex => indices.GetActionIndexForMode(curIndex); //for meta saving on mode switch only? public int CurMetaIndex => indices.GetMetaIndexForMode(curIndex); public int ModeCount => indices.modeCount; //mapping object is created on StartHolding //we set the curIndex field instead of the property, according to following situations: //1. it's a newly created ItemValue, meta and ammo index belongs to action0, no saving is needed; //2. it's an existing ItemValue, meta and ammo index is set to its action index, still saving is unnecessary. internal MultiActionMapping(ActionModuleAlternative.AlternativeData altData, MultiActionIndice indices, EntityAlive entity, ItemValue itemValueTemp, string toggleSound, int slotIndex, bool[] unlocked) { this.altData = altData; this.indices = indices; this.entity = entity; this.slotIndex = slotIndex; this.unlocked = unlocked; object res = itemValueTemp.GetMetadata(STR_MULTI_ACTION_INDEX); if (res is false || res is null) { itemValueTemp.SetMetadata(STR_MULTI_ACTION_INDEX, 0, TypedMetadataValue.TypeTag.Integer); curIndex = 0; } else { curIndex = (int)res; ReadMeta(); } unsafe { for (int i = 0; i < MultiActionIndice.MAX_ACTION_COUNT; i++) { int metaIndex = indices.metaIndice[i]; if (metaIndex < 0) break; if (!itemValueTemp.HasMetadata(MultiActionUtils.ActionMetaNames[metaIndex])) { itemValueTemp.SetMetadata(MultiActionUtils.ActionMetaNames[metaIndex], 0, TypedMetadataValue.TypeTag.Integer); } #if DEBUG else { Log.Out($"{MultiActionUtils.ActionMetaNames[metaIndex]}: {itemValueTemp.GetMetadata(MultiActionUtils.ActionMetaNames[metaIndex]).ToString()}"); } #endif if (!itemValueTemp.HasMetadata(MultiActionUtils.ActionSelectedAmmoNames[metaIndex])) { itemValueTemp.SetMetadata(MultiActionUtils.ActionSelectedAmmoNames[metaIndex], 0, TypedMetadataValue.TypeTag.Integer); } #if DEBUG else { Log.Out($"{MultiActionUtils.ActionSelectedAmmoNames[metaIndex]}: {itemValueTemp.GetMetadata(MultiActionUtils.ActionSelectedAmmoNames[metaIndex]).ToString()}"); } #endif } } this.toggleSound = toggleSound; entity.emodel?.avatarController?.UpdateInt(MultiActionUtils.ExecutingActionIndexHash, CurActionIndex, false); #if DEBUG Log.Out($"MultiAction mode {curIndex}, meta {itemValueTemp.Meta}, ammo index {itemValueTemp.SelectedAmmoTypeIndex}\n {StackTraceUtility.ExtractStackTrace()}"); #endif } public void SaveMeta(ItemValue _itemValue = null) { //save previous meta and ammo index to metadata int curMetaIndex = CurMetaIndex; ItemValue itemValue = _itemValue ?? ItemValue; if (itemValue == null) return; ItemAction[] actions = itemValue.ItemClass.Actions; if (CurActionIndex < 0 || CurActionIndex >= actions.Length) return; ItemActionAttack itemActionAttack = actions[CurActionIndex] as ItemActionAttack; if (itemActionAttack == null) return; if (ConsoleCmdReloadLog.LogInfo) { Log.Out($"Saving meta for item {itemValue.ItemClass.Name}"); } itemValue.SetMetadata(MultiActionUtils.ActionMetaNames[curMetaIndex], itemValue.Meta, TypedMetadataValue.TypeTag.Integer); itemValue.SetMetadata(MultiActionUtils.ActionSelectedAmmoNames[curMetaIndex], (int)itemValue.SelectedAmmoTypeIndex, TypedMetadataValue.TypeTag.Integer); if (itemValue.SelectedAmmoTypeIndex > itemActionAttack.MagazineItemNames.Length) { Log.Error($"SAVING META ERROR: AMMO INDEX LARGER THAN AMMO ITEM COUNT!\n{StackTraceUtility.ExtractStackTrace()}"); } if (ConsoleCmdReloadLog.LogInfo) { ConsoleCmdMultiActionItemValueDebug.LogMeta(itemValue); Log.Out($"Save Meta stacktrace:\n{StackTraceUtility.ExtractStackTrace()}"); } } public void ReadMeta(ItemValue _itemValue = null) { int curMetaIndex = CurMetaIndex; ItemValue itemValue = _itemValue ?? ItemValue; if (itemValue == null) return; itemValue.SetMetadata(STR_MULTI_ACTION_INDEX, curIndex, TypedMetadataValue.TypeTag.Integer); object res = itemValue.GetMetadata(MultiActionUtils.ActionMetaNames[curMetaIndex]); if (res is false || res is null) { itemValue.SetMetadata(MultiActionUtils.ActionMetaNames[curMetaIndex], 0, TypedMetadataValue.TypeTag.Integer); itemValue.Meta = 0; } else { itemValue.Meta = (int)res; } res = itemValue.GetMetadata(MultiActionUtils.ActionSelectedAmmoNames[curMetaIndex]); if (res is false || res is null) { itemValue.SetMetadata(MultiActionUtils.ActionSelectedAmmoNames[curMetaIndex], 0, TypedMetadataValue.TypeTag.Integer); itemValue.SelectedAmmoTypeIndex = 0; } else { itemValue.SelectedAmmoTypeIndex = (byte)(int)res; } if (ConsoleCmdReloadLog.LogInfo) { ConsoleCmdMultiActionItemValueDebug.LogMeta(itemValue); Log.Out($"Read Meta stacktrace:\n{StackTraceUtility.ExtractStackTrace()}"); } } public int SetupRadial(XUiC_Radial _xuiRadialWindow, EntityPlayerLocal _epl) { _xuiRadialWindow.ResetRadialEntries(); int preSelectedIndex = -1; string[] magazineItemNames = ((ItemActionAttack)_epl.inventory.holdingItem.Actions[CurActionIndex]).MagazineItemNames; bool[] disableStates = CommonUtilityPatch.GetUnusableItemEntries(magazineItemNames, _epl, CurActionIndex); for (int i = 0; i < magazineItemNames.Length; i++) { ItemClass ammoClass = ItemClass.GetItemClass(magazineItemNames[i], false); if (ammoClass != null && (!_epl.isHeadUnderwater || ammoClass.UsableUnderwater) && !disableStates[i]) { int ammoCount = _xuiRadialWindow.xui.PlayerInventory.GetItemCount(ammoClass.Id); bool isCurrentUsing = _epl.inventory.holdingItemItemValue.SelectedAmmoTypeIndex == i; _xuiRadialWindow.CreateRadialEntry(i, ammoClass.GetIconName(), (ammoCount > 0) ? "ItemIconAtlas" : "ItemIconAtlasGreyscale", ammoCount.ToString(), ammoClass.GetLocalizedItemName(), isCurrentUsing); if (isCurrentUsing) { preSelectedIndex = i; } } } return preSelectedIndex; } public bool CheckDisplayMode() { if (lastDisplayMode == CurMode) { return false; } else { lastDisplayMode = CurMode; return true; } } public bool IsActionUnlocked(int actionIndex) { if (actionIndex >= MultiActionIndice.MAX_ACTION_COUNT || actionIndex < 0) return false; return unlocked[actionIndex]; } } public static class MultiActionManager { //clear on game load private static readonly Dictionary dict_mappings = new Dictionary(); private static readonly Dictionary dict_indice = new Dictionary(); private static readonly Dictionary[]> dict_item_action_exclude_tags = new Dictionary[]>(); private static readonly Dictionary dict_item_action_exclude_mod_property = new Dictionary(); private static readonly Dictionary dict_item_action_exclude_mod_passive = new Dictionary(); private static readonly Dictionary dict_item_action_exclude_mod_trigger = new Dictionary(); //should set to true when: //mode switch input received; //start holding new multi action weapon.? //if true, send local curIndex to other clients in updateSendClientPlayerPositionToServer. public static bool LocalModeChanged { get; set; } public static void PostloadCleanup() { dict_mappings.Clear(); dict_indice.Clear(); } public static void PreloadCleanup() { dict_item_action_exclude_tags.Clear(); dict_item_action_exclude_mod_property.Clear(); dict_item_action_exclude_mod_passive.Clear(); dict_item_action_exclude_mod_trigger.Clear(); } public static void ParseItemActionExcludeTagsAndModifiers(ItemClass item) { if (item == null) return; FastTags[] tags = null; int[][] properties = null, passives = null, triggers = null; for (int i = 0; i < item.Actions.Length; i++) { if (item.Actions[i] != null) { if (item.Actions[i].Properties.Values.TryGetValue("ExcludeTags", out string str)) { if (tags == null) { tags = new FastTags[ItemClass.cMaxActionNames]; dict_item_action_exclude_tags.Add(item.Id, tags); } tags[i] = FastTags.Parse(str); } if (item.Actions[i].Properties.Values.TryGetValue("ExcludeMods", out str)) { if (properties == null) { properties = new int[ItemClass.cMaxActionNames][]; dict_item_action_exclude_mod_property.Add(item.Id, properties); } properties[i] = str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(s => !string.IsNullOrEmpty(s)) .Select(s => ItemClass.GetItemClass(s, false)) .Where(_item => _item != null) .Select(_item => _item.Id) .ToArray(); //Log.Out($"EXCLUDE PROPERTIES FROM ITEM {item.Name} ITEMID {item.Id} ACTION {i} : {string.Join(" ", properties[i])}"); } if (item.Actions[i].Properties.Values.TryGetValue("ExcludePassives", out str)) { if (passives == null) { passives = new int[ItemClass.cMaxActionNames][]; dict_item_action_exclude_mod_passive.Add(item.Id, passives); } passives[i] = str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(s => !string.IsNullOrEmpty(s)) .Select(s => ItemClass.GetItemClass(s, false)) .Where(_item => _item != null) .Select(_item => _item.Id) .ToArray(); //Log.Out($"EXCLUDE PASSIVES FROM ITEM {item.Name} ITEMID {item.Id} ACTION {i} : {string.Join(" ", passives[i])}"); } if (item.Actions[i].Properties.Values.TryGetValue("ExcludeTriggers", out str)) { if (triggers == null) { triggers = new int[ItemClass.cMaxActionNames][]; dict_item_action_exclude_mod_trigger.Add(item.Id, triggers); } triggers[i] = str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(s => !string.IsNullOrEmpty(s)) .Select(s => ItemClass.GetItemClass(s, false)) .Where(_item => _item != null) .Select(_item => _item.Id) .ToArray(); //Log.Out($"EXCLUDE TRIGGERS FROM ITEM {item.Name} ITEMID {item.Id} ACTION {i} : {string.Join(" ", triggers[i])}"); } } } } public static void ModifyItemTags(ItemValue itemValue, ItemActionData actionData, ref FastTags tags) { if (itemValue == null || actionData == null || !dict_item_action_exclude_tags.TryGetValue(itemValue.type, out var arr_tags)) { return; } tags = tags.Remove(arr_tags[actionData.indexInEntityOfAction]); } public static bool ShouldExcludeProperty(int itemId, int modId, int actionIndex) { return dict_item_action_exclude_mod_property.TryGetValue(itemId, out var arr_exclude) && arr_exclude[actionIndex] != null && Array.IndexOf(arr_exclude[actionIndex], modId) >= 0; } public static bool ShouldExcludePassive(int itemId, int modId, int actionIndex) { return dict_item_action_exclude_mod_passive.TryGetValue(itemId, out var arr_exclude) && arr_exclude[actionIndex] != null && Array.IndexOf(arr_exclude[actionIndex], modId) >= 0; } public static bool ShouldExcludeTrigger(int itemId, int modId, int actionIndex) { return dict_item_action_exclude_mod_trigger.TryGetValue(itemId, out var arr_exclude) && arr_exclude[actionIndex] != null && Array.IndexOf(arr_exclude[actionIndex], modId) >= 0; } public static void UpdateLocalMetaSave(int playerID) { if (dict_mappings.TryGetValue(playerID, out MultiActionMapping mapping)) { mapping?.SaveMeta(); } } public static MultiActionIndice GetActionIndiceForItemID(int itemID) { if (dict_indice.TryGetValue(itemID, out MultiActionIndice indice)) return indice; ItemClass item = ItemClass.GetForId(itemID); if (item == null) return indice; indice = new MultiActionIndice(item); dict_indice[itemID] = indice; return indice; } public static void ToggleLocalActionIndex(EntityPlayerLocal player) { if (player == null || !dict_mappings.TryGetValue(player.entityId, out MultiActionMapping mapping)) return; if (mapping.ModeCount <= 1 || player.inventory.IsHoldingItemActionRunning()) return; int prevMode = mapping.CurMode; mapping.CurMode++; if (prevMode != mapping.CurMode) { FireToggleModeEvent(player, mapping); player.inventory.CallOnToolbeltChangedInternal(); } } public static void FireToggleModeEvent(EntityPlayerLocal player, MultiActionMapping mapping) { player.PlayOneShot(mapping.toggleSound); LocalModeChanged = true; player.MinEventContext.ItemActionData = player.inventory.holdingItemData.actionData[mapping.CurActionIndex]; player.FireEvent(CustomEnums.onSelfItemSwitchMode); } public static void SetMappingForEntity(int entityID, MultiActionMapping mapping) { dict_mappings[entityID] = mapping; //Log.Out($"current item index mapping: {((mapping == null || mapping.itemValue == null) ? "null" : mapping.itemValue.ItemClass.Name)}"); } public static bool SetModeForEntity(int entityID, int mode) { if (dict_mappings.TryGetValue(entityID, out MultiActionMapping mapping) && mapping != null) { int prevMode = mapping.CurMode; mapping.CurMode = mode; return prevMode != mapping.CurMode; } return false; } public static int GetModeForEntity(int entityID) { if (!dict_mappings.TryGetValue(entityID, out MultiActionMapping mapping) || mapping == null) return 0; return mapping.CurMode; } public static int GetActionIndexForEntity(EntityAlive entity) { if (entity == null || !dict_mappings.TryGetValue(entity.entityId, out var mapping) || mapping == null) return 0; return mapping.CurActionIndex; } public static int GetMetaIndexForEntity(int entityID) { if (!dict_mappings.TryGetValue(entityID, out var mapping) || mapping == null) return 0; return mapping.CurMetaIndex; } public static int GetMetaIndexForActionIndex(int entityID, int actionIndex) { if (!dict_mappings.TryGetValue(entityID, out var mapping) || mapping == null) { return actionIndex; } int mode = mapping.indices.GetModeForAction(actionIndex); if (mode > 0) { unsafe { return mapping.indices.metaIndice[mode]; } } return actionIndex; } public static MultiActionMapping GetMappingForEntity(int entityID) { dict_mappings.TryGetValue(entityID, out var mapping); return mapping; } internal static float inputCD = 0; internal static void UpdateLocalInput(EntityPlayerLocal player, PlayerActionsLocal localActions, bool isUIOpen, float _dt) { if (inputCD > 0) { inputCD = Math.Max(0, inputCD - _dt); } if (isUIOpen || inputCD > 0 || player.emodel.IsRagdollActive || player.IsDead() || player.AttachedToEntity != null) { return; } if (PlayerActionKFLib.Instance.ToggleActionMode && PlayerActionKFLib.Instance.ToggleActionMode.WasPressed) { var mapping = GetMappingForEntity(player.entityId); if (mapping == null) { return; } if (player.inventory.IsHoldingItemActionRunning()) { return; } if (localActions.Reload.WasPressed || localActions.PermanentActions.Reload.WasPressed) { inputCD = 0.1f; return; } player.inventory.Execute(mapping.CurActionIndex, true, localActions); localActions.Primary.ClearInputState(); ToggleLocalActionIndex(player); } } } }