Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions API/Client/IExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;

namespace Bloodstone.API.Client;
public static class IExtensions
{
public static Dictionary<TValue, TKey> Reverse<TKey, TValue>(
this IDictionary<TKey, TValue> source)
{
var reversed = new Dictionary<TValue, TKey>();

foreach (var kvp in source)
{
reversed[kvp.Value] = kvp.Key;
}

return reversed;
}
public static Dictionary<TValue, TKey> ReverseIl2CppDictionary<TKey, TValue>(
this Il2CppSystem.Collections.Generic.Dictionary<TKey, TValue> source)
{
var reversed = new Dictionary<TValue, TKey>();

if (source == null) return reversed;

foreach (var kvp in source)
{
if (reversed.ContainsKey(kvp.Value))
{
continue;
}

reversed[kvp.Value] = kvp.Key;
}

return reversed;
}
public static void ForEach<T>(this IEnumerable<T> collection, Action<T> action)
{
foreach (var item in collection)
{
action(item);
}
}
public static bool IsIndexWithinRange<T>(this IList<T> list, int index)
{
return index >= 0 && index < list.Count;
}
public static bool ContainsAll(this string stringChars, List<string> strings)
{
foreach (string str in strings)
{
if (!stringChars.Contains(str, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}

return true;
}
public static bool ContainsAny(this string stringChars, List<string> strings)
{
foreach (string str in strings)
{
if (stringChars.Contains(str, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}
}
143 changes: 143 additions & 0 deletions API/Client/KeybindManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using Bloodstone.Util;
using ProjectM;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

namespace Bloodstone.API.Client;
public static class KeybindManager
{
public static IReadOnlyDictionary<string, Keybinding> Keybinds => _keybinds;
static readonly Dictionary<string, Keybinding> _keybinds = [];

const ulong HASH_LONG = 14695981039346656037UL;
const uint HASH_INT = 2166136261U;

static readonly Dictionary<KeyCode, string> _keyLiterals = new()
{
{ KeyCode.Space, " " },
{ KeyCode.BackQuote, "`" },
{ KeyCode.Minus, "-" },
{ KeyCode.Equals, "=" },
{ KeyCode.LeftBracket, "[" },
{ KeyCode.RightBracket, "]" },
{ KeyCode.Backslash, "\\" },
{ KeyCode.Semicolon, ";" },
{ KeyCode.Quote, "'" },
{ KeyCode.Comma, "," },
{ KeyCode.Period, "." },
{ KeyCode.Slash, "/" },
{ KeyCode.Alpha0, "0" },
{ KeyCode.Alpha1, "1" },
{ KeyCode.Alpha2, "2" },
{ KeyCode.Alpha3, "3" },
{ KeyCode.Alpha4, "4" },
{ KeyCode.Alpha5, "5" },
{ KeyCode.Alpha6, "6" },
{ KeyCode.Alpha7, "7" },
{ KeyCode.Alpha8, "8" },
{ KeyCode.Alpha9, "9" },
{ KeyCode.A, "A" },
{ KeyCode.B, "B" },
{ KeyCode.C, "C" },
{ KeyCode.D, "D" },
{ KeyCode.E, "E" },
{ KeyCode.F, "F" },
{ KeyCode.G, "G" },
{ KeyCode.H, "H" },
{ KeyCode.I, "I" },
{ KeyCode.J, "J" },
{ KeyCode.K, "K" },
{ KeyCode.L, "L" },
{ KeyCode.M, "M" },
{ KeyCode.N, "N" },
{ KeyCode.O, "O" },
{ KeyCode.P, "P" },
{ KeyCode.Q, "Q" },
{ KeyCode.R, "R" },
{ KeyCode.S, "S" },
{ KeyCode.T, "T" },
{ KeyCode.U, "U" },
{ KeyCode.V, "V" },
{ KeyCode.W, "W" },
{ KeyCode.X, "X" },
{ KeyCode.Y, "Y" },
{ KeyCode.Z, "Z" },
{ KeyCode.UpArrow, "↑" },
{ KeyCode.DownArrow, "↓" },
{ KeyCode.LeftArrow, "←" },
{ KeyCode.RightArrow, "→" }
};
public static Keybinding Register(string name, string description, string category, KeyCode defaultKey)
{
if (_keybinds.TryGetValue(name, out var existing))
{
// Core.Log.LogInfo($"[KeybindsManager] Skipped duplicate keybind registration: {name}");
return existing;
}

var keybind = new Keybinding(name, description, category, defaultKey);
_keybinds[name] = keybind;

return keybind;
}
public static void Rebind(Keybinding keybind, KeyCode newKey)
{
keybind.Primary = newKey;
Persistence.SaveKeybinds();
}
public static ButtonInputAction ComputeInputFlag(string descriptionId)
{
byte[] bytes = Encoding.UTF8.GetBytes(descriptionId);
ulong num = Hash64(bytes);
bool flag = false;

do
{
foreach (ButtonInputAction buttonInputAction in Enum.GetValues<ButtonInputAction>())
{
if (num == (ulong)buttonInputAction)
{
flag = true;
num--;
}
}
} while (flag);

return (ButtonInputAction)num;
}
public static int ComputeAssetGuid(string descriptionId)
{
byte[] bytes = Encoding.UTF8.GetBytes(descriptionId);
return (int)Hash32(bytes);
}
public static string GetLiteral(KeyCode key)
{
return _keyLiterals.TryGetValue(key, out var literal) ? literal : key.ToString();
}
static ulong Hash64(byte[] data)
{
ulong hash = HASH_LONG;

foreach (var b in data)
{
hash ^= b;
hash *= 1099511628211UL;
}

return hash;
}
static uint Hash32(byte[] data)
{
uint hash = HASH_INT;

foreach (var b in data)
{
hash ^= b;
hash *= 16777619U;
}

return hash;
}
}
83 changes: 83 additions & 0 deletions API/Client/Keybinding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using ProjectM;
using Stunlock.Localization;
using System;
using System.Text.Json.Serialization;
using UnityEngine;

namespace Bloodstone.API.Client;

/// <summary>
/// Properly hooking keybinding menu in V Rising is a major pain in the ass. The
/// geniuses over at Stunlock studios decided to make the keybindings a flag enum.
/// This sounds decent, but it locks you to a whopping 64 unique keybindings. Guess
/// how many the game uses? 64 exactly.
///
/// As a result we can't just hook into the same system and add a new control, since
/// we don't actually have any free keybinding codes we could re-use. If we tried to
/// do that, it would mean that if a user used one of our keybinds, they would also
/// use at least one of the pre-configured game keybinds (since the IsKeyDown check
/// only checks whether the specific bit in the current input bitfield is set). As a
/// result we have to work around this by carefully avoiding that our custom invalid
/// keybinding flags are never serialized to the input system that V Rising uses, so
/// we have to implement quite a bit ourselves. This will probably break at some point
/// since I doubt Stunlock will be content with 64 unique input settings for the rest
/// of the game's lifetime. Good luck for who will end up needing to fix it.
/// </summary>

[Serializable]
public class Keybinding
{
public string Name;
public string Description;
public string Category;

public KeyCode Primary = KeyCode.None;
public string PrimaryName => KeybindManager.GetLiteral(Primary);

// public KeyCode Secondary = KeyCode.None;
// public string SecondaryName => KeybindManager.GetLiteral(Secondary);

public delegate void KeyHandler();

public event KeyHandler OnKeyPressedHandler = delegate { };
public event KeyHandler OnKeyDownHandler = delegate { };
public event KeyHandler OnKeyUpHandler = delegate { };

[JsonIgnore]
public LocalizationKey NameKey;

[JsonIgnore]
public LocalizationKey DescriptionKey;

[JsonIgnore]
public ButtonInputAction InputFlag;

[JsonIgnore]
public int AssetGuid;

// public Keybinding() { }
public Keybinding(string name, string description, string category, KeyCode defaultKey)
{
Name = name;
Description = description;
Category = category;
Primary = defaultKey;
NameKey = LocalizationKeyManager.GetLocalizationKey(name);
DescriptionKey = LocalizationKeyManager.GetLocalizationKey(description);
InputFlag = KeybindManager.ComputeInputFlag(name);
AssetGuid = KeybindManager.ComputeAssetGuid(name);
}
public void AddKeyPressedListener(KeyHandler action) => OnKeyPressedHandler += action;
public void AddKeyDownListener(KeyHandler action) => OnKeyDownHandler += action;
public void AddKeyUpListener(KeyHandler action) => OnKeyUpHandler += action;
public void KeyPressed() => OnKeyPressedHandler();
public void KeyDown() => OnKeyDownHandler();
public void KeyUp() => OnKeyUpHandler();
public void ApplySaved(Keybinding keybind)
{
if (keybind == null) return;

Primary = keybind.Primary;
// Secondary = keybind.Secondary;
}
}
45 changes: 45 additions & 0 deletions API/Client/LocalizationKeyManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Stunlock.Core;
using Stunlock.Localization;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Guid = Il2CppSystem.Guid;

namespace Bloodstone.API.Client;
public static class LocalizationKeyManager
{
const string KEYBINDS_HEADER = MyPluginInfo.PLUGIN_NAME;
public static LocalizationKey _sectionHeader;

// public static IReadOnlyDictionary<string, LocalizationKey> SectionHeaders => _sectionHeaders;
// static readonly Dictionary<string, LocalizationKey> _sectionHeaders = [];
public static IReadOnlyDictionary<AssetGuid, string> AssetGuids => _assetGuids;
static readonly Dictionary<AssetGuid, string> _assetGuids = [];
public static void LocalizeText()
{
_sectionHeader = GetLocalizationKey(KEYBINDS_HEADER);

foreach (var keyValuePair in AssetGuids)
{
AssetGuid assetGuid = keyValuePair.Key;
string localizedString = keyValuePair.Value;

Localization._LocalizedStrings.TryAdd(assetGuid, localizedString);
}
}
static AssetGuid GetAssetGuid(string text)
{
using SHA256 sha256 = SHA256.Create();
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));

Guid uniqueGuid = new(hashBytes[..16]);
return AssetGuid.FromGuid(uniqueGuid);
}
public static LocalizationKey GetLocalizationKey(string value)
{
AssetGuid assetGuid = GetAssetGuid(value);
_assetGuids.TryAdd(assetGuid, value);

return new(assetGuid);
}
}
Loading