diff --git a/BepInEx.Hacknet/BepInEx.Hacknet.csproj b/BepInEx.Hacknet/BepInEx.Hacknet.csproj
index 93e14800..98add7bd 100644
--- a/BepInEx.Hacknet/BepInEx.Hacknet.csproj
+++ b/BepInEx.Hacknet/BepInEx.Hacknet.csproj
@@ -19,6 +19,7 @@
+
s.Length);
+
+ Console.WriteLine("#" + new string('=', l+2) + "#");
+ foreach (string line in lines)
+ Console.WriteLine("| " + Center(line,l) + " |");
+ Console.WriteLine("#" + new string('=', l+2) + "#");
+ }
+ bool hasDLC = File.Exists("Content/DLC/DLCFaction.xml");
+ bool isSteam = typeof(HN.PlatformAPI.Storage.SteamCloudStorageMethod).GetField("deserialized") != null;
+ WriteInBox
+ (
+ $"Hacknet {(hasDLC ? "+Labyrinths " : "")}{(isSteam ? "Steam" : "Non-Steam")} {HN.MainMenu.OSVersion}",
+ $"{Environment.OSVersion.Platform} ({SDL2.SDL.SDL_GetPlatform()})",
+ $"Chainloader {HacknetChainloader.VERSION}"
+ );
+ }
+
public static Assembly ResolveBepAssembly(object sender, ResolveEventArgs args)
{
var asmName = new AssemblyName(args.Name);
diff --git a/Configurations.props b/Configurations.props
index ccce199d..9d842119 100644
--- a/Configurations.props
+++ b/Configurations.props
@@ -33,6 +33,7 @@
10
enable
+ true
false
$(MSBuildThisFileDirectory)
$(PathfinderSolutionDir)libs/
diff --git a/Linux/StartPathfinder.sh b/Linux/StartPathfinder.sh
index a9b5ab7a..75750888 100644
--- a/Linux/StartPathfinder.sh
+++ b/Linux/StartPathfinder.sh
@@ -1,7 +1,7 @@
-cd "`dirname "$0"`"
+cd "$(dirname "$0")"
if [ "$(uname -m)" == "x86_64" ]; then
- LD_PRELOAD="$(pwd)/lib64/libcef.so $(pwd)/intercept.so /usr/lib/libmono-2.0.so" MONO_DEBUG=explicit-null-checks ./HacknetPathfinder.bin.x86_64 $@
+ LD_PRELOAD="$(pwd)/intercept.so /usr/lib/libmono-2.0.so" MONO_DEBUG=explicit-null-checks ./HacknetPathfinder.bin.x86_64 "$@"
else
- LD_PRELOAD="$(pwd)/lib/libcef.so $(pwd)/intercept.so /usr/lib/libmono-2.0.so" MONO_DEBUG=explicit-null-checks ./HacknetPathfinder.bin.x86 $@
+ LD_PRELOAD="$(pwd)/intercept.so /usr/lib/libmono-2.0.so" MONO_DEBUG=explicit-null-checks ./HacknetPathfinder.bin.x86 "$@"
fi
diff --git a/PathfinderAPI/PathfinderAPI.csproj b/PathfinderAPI/PathfinderAPI.csproj
index 853862da..14de8338 100644
--- a/PathfinderAPI/PathfinderAPI.csproj
+++ b/PathfinderAPI/PathfinderAPI.csproj
@@ -17,6 +17,10 @@
+
+
+
+
diff --git a/PathfinderAPI/PathfinderAPIPlugin.cs b/PathfinderAPI/PathfinderAPIPlugin.cs
index 1d28642a..269407f5 100644
--- a/PathfinderAPI/PathfinderAPIPlugin.cs
+++ b/PathfinderAPI/PathfinderAPIPlugin.cs
@@ -30,6 +30,8 @@ public override bool Load()
PathfinderAPIPlugin.HarmonyInstance = base.HarmonyInstance;
Logger.LogSource = base.Log;
PathfinderAPIPlugin.Config = base.Config;
+
+ Environment.SetEnvironmentVariable("LD_PRELOAD", $"./lib{(Environment.Is64BitProcess ? "64" : "")}/libcef.so");
foreach (var initMethod in typeof(PathfinderAPIPlugin).Assembly.GetTypes().SelectMany(AccessTools.GetDeclaredMethods))
{
diff --git a/PathfinderAPI/Replacements/ActionsLoader.cs b/PathfinderAPI/Replacements/ActionsLoader.cs
index 56543af0..d9e4e03f 100644
--- a/PathfinderAPI/Replacements/ActionsLoader.cs
+++ b/PathfinderAPI/Replacements/ActionsLoader.cs
@@ -196,7 +196,7 @@ public static SerializableAction ReadAction(ElementInfo actionInfo)
case "AddIRCMessage":
return new SAAddIRCMessage()
{
- Author = ComputerLoader.filter(actionInfo.Attributes.GetOrThrow("Author", "Invalid author for AddIRCMessage", StringExtensions.HasContent)),
+ Author = ComputerLoader.filter(actionInfo.Attributes.GetString("Author")),
Message = ComputerLoader.filter(string.IsNullOrEmpty(actionInfo.Content) ? throw new FormatException("Invalid message for AddIRCMessage") : actionInfo.Content),
Delay = actionInfo.Attributes.GetFloat("Delay"),
TargetComp = actionInfo.Attributes.GetOrThrow("TargetComp", "Invalid target computer for AddIRCMessage", StringExtensions.HasContent)
diff --git a/PathfinderAPI/Replacements/FileEncrypterReplacement.cs b/PathfinderAPI/Replacements/FileEncrypterReplacement.cs
new file mode 100644
index 00000000..decbd0dc
--- /dev/null
+++ b/PathfinderAPI/Replacements/FileEncrypterReplacement.cs
@@ -0,0 +1,277 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using Hacknet;
+using HarmonyLib;
+using Mono.Cecil.Cil;
+using MonoMod.Cil;
+using Pathfinder.Util;
+
+namespace Pathfinder.Replacements;
+
+[HarmonyPatch]
+internal static class FileEncrypterReplacement
+{
+ // Two things need to be replaced here
+ // First, decryption needs to be attempted at max 4 times for all three hash types when it wasn't created by Pathfinder.
+ // 1. Windows .NET Framework 64 bit's hash
+ // 2. Windows .NET Framework 32 bit's hash (yes, they're different)
+ // 3. Mono's older hash (the one shipped with Hacknet on Linux, newer Mono uses the NetFX hash (i think))
+ // 4. as a last resort, try GetHashCode (bleh)
+ // Second, encryption should *always* use Pathfinder's stable hash.
+ // Files encrypted by Pathfinder will get an extra header section just with PATHFINDER, so we can tell which are stable.
+ // Stable hash I'm using here is FNV1a xor-folded to 16 bits, for its simplicity. https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
+ // We don't need anything good (we only have 16 bits in the first place), I don't care that its not for cryptographic use
+ // ALL I care about is that the hash is *stable*.
+ // - Aaron
+
+ // Just keep all the legacy hash functions in an array to make this easy to loop over
+ private static readonly Func[] _hashFunctions = { Fx64Hash, MonoHash, Fx32Hash, GetHashCodeHashBad };
+
+ private static readonly ushort Fnv1aEmptyHash = Fnv1aHash(string.Empty);
+
+ // *technically* an extension could have this as a file extension, but... i dont think thats going to happen
+ private static readonly string PathfinderNeedle = FileEncrypter.Encrypt("PATHFINDER", Fnv1aHash("PATHFINDER"));
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(FileEncrypter), nameof(FileEncrypter.DecryptString))]
+ private static bool DecryptStringPrefix(string data, string pass, out string[] __result)
+ {
+ if (string.IsNullOrEmpty(data))
+ {
+ throw new NullReferenceException("String to decrypt cannot be null or empty");
+ }
+ var lines = data.Split(Utils.robustNewlineDelim, StringSplitOptions.RemoveEmptyEntries);
+ if (lines.Length < 2)
+ {
+ throw new FormatException("Tried to decrypt an invalid valid DEC ENC file \"" + data + "\" - not enough elements. Need 2 lines, had " + lines.Length);
+ }
+ var headerParts = lines[0].Split(FileEncrypter.HeaderSplitDelimiters, StringSplitOptions.None);
+ if (headerParts.Length < 4)
+ {
+ throw new FormatException("Tried to decrypt an invalid valid DEC ENC file \"" + data + "\" - not enough headers");
+ }
+
+ if (headerParts[headerParts.Length - 1] == PathfinderNeedle)
+ {
+ // we know this is FNV1a
+ var hash = Fnv1aHash(pass);
+ var passcodeCheck = FileEncrypter.Decrypt(headerParts[3], hash);
+ var correctPass = passcodeCheck == "ENCODED";
+ __result = new[]
+ {
+ // header text
+ FileEncrypter.Decrypt(headerParts[1], Fnv1aEmptyHash),
+ // IP address
+ FileEncrypter.Decrypt(headerParts[2], Fnv1aEmptyHash),
+ // actual content
+ correctPass ? FileEncrypter.Decrypt(lines[1], hash) : null,
+ // extension
+ headerParts.Length > 5 ? FileEncrypter.Decrypt(headerParts[4], Fnv1aEmptyHash) : null,
+ // success marker
+ correctPass ? "1" : "0",
+ // whatever the decryption attempt for ENCODED came back as (even if it failed)
+ passcodeCheck
+ };
+ return false;
+ }
+
+ foreach (var hashFunction in _hashFunctions)
+ {
+ ushort attempedHash = hashFunction(pass);
+ // this can still hypothetically be wrong if there is a collision between the hash functions
+ // but i really just do not care at that point, sorry for the people playing on vanilla saves with pathfinder who hit those odds
+ var decodedMarker = FileEncrypter.Decrypt(headerParts[3], attempedHash);
+ var correct = decodedMarker == "ENCODED";
+ if (correct)
+ {
+ var emptyHash = hashFunction(string.Empty);
+ __result = new[]
+ {
+ FileEncrypter.Decrypt(headerParts[1], emptyHash),
+ FileEncrypter.Decrypt(headerParts[2], emptyHash),
+ FileEncrypter.Decrypt(lines[1], attempedHash),
+ headerParts.Length > 4 ? FileEncrypter.Decrypt(headerParts[4], emptyHash) : null,
+ "1",
+ decodedMarker
+ };
+ return false;
+ }
+ }
+
+ // password wasn't correct, time to guess at header decoding! :)
+ // the best way i can think of to do this is to:
+ // 1. IP is
+ // a. "ERROR" (default value)
+ // b. exists on the netmap
+ // c. just contains three dots (xxx.xxx.xxx.xxx)
+ // 2. header text is "ERROR" for good measure
+ // should cover most cases
+ foreach (var hashFunction in _hashFunctions)
+ {
+ var emptyHash = hashFunction(string.Empty);
+ var decodedIp = FileEncrypter.Decrypt(headerParts[2], emptyHash);
+ var headerText = FileEncrypter.Decrypt(headerParts[1], emptyHash);
+ if (decodedIp == "ERROR" || ComputerLookup.FindByIp(decodedIp, false) != null || decodedIp.Count(c => c == '.') == 3 || headerText == "ERROR")
+ {
+ __result = new[]
+ {
+ headerText,
+ decodedIp,
+ null,
+ headerParts.Length > 4 ? FileEncrypter.Decrypt(headerParts[4], emptyHash) : null,
+ "0",
+ FileEncrypter.Decrypt(headerParts[3], hashFunction(pass))
+ };
+ return false;
+ }
+ }
+
+ // give up, use GetHashCode to give back something for headers :(
+ var passHashBad = GetHashCodeHashBad(pass);
+ var emptyHashBad = GetHashCodeHashBad(string.Empty);
+ __result = new[]
+ {
+ FileEncrypter.Decrypt(headerParts[1], emptyHashBad),
+ FileEncrypter.Decrypt(headerParts[2], emptyHashBad),
+ null,
+ headerParts.Length > 4 ? FileEncrypter.Decrypt(headerParts[4], emptyHashBad) : null,
+ "0",
+ FileEncrypter.Decrypt(headerParts[3], passHashBad)
+ };
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(FileEncrypter), nameof(FileEncrypter.DecryptHeaders))]
+ private static bool DecryptHeadersPrefix(string data, string pass, out string[] __result)
+ {
+ // i'm too lazy to actually implement this.
+ var fullDecrypt = FileEncrypter.DecryptString(data, pass);
+ __result = new[]
+ {
+ fullDecrypt[0],
+ fullDecrypt[1],
+ fullDecrypt[3]
+ };
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(FileEncrypter), nameof(FileEncrypter.GetPassCodeFromString))]
+ private static bool GetPassCodeFromStringReplacement(string code, out ushort __result)
+ {
+ __result = Fnv1aHash(code);
+ return false;
+ }
+
+ [HarmonyILManipulator]
+ [HarmonyPatch(typeof(FileEncrypter), nameof(FileEncrypter.EncryptString))]
+ private static void EncryptStringIL(ILContext il)
+ {
+ var c = new ILCursor(il);
+
+ var appendMethod = AccessTools.DeclaredMethod(typeof(StringBuilder), nameof(StringBuilder.Append), new[] { typeof(string) });
+
+ c.GotoNext(MoveType.After,
+ x => x.MatchCallvirt(appendMethod),
+ x => x.MatchPop()
+ );
+
+ c.Emit(OpCodes.Ldloc_2);
+ c.Emit(OpCodes.Ldstr, "::");
+ c.Emit(OpCodes.Callvirt, appendMethod);
+ c.Emit(OpCodes.Ldsfld, AccessTools.DeclaredField(typeof(FileEncrypterReplacement), nameof(PathfinderNeedle)));
+ c.Emit(OpCodes.Callvirt, appendMethod);
+ c.Emit(OpCodes.Pop);
+ }
+
+ private static ushort Fnv1aHash(string str)
+ {
+ const uint OFFSET_BASIS = 2166136261;
+ const uint FNV_PRIME = 16777619;
+
+ uint hash = OFFSET_BASIS;
+
+ foreach (var b in MemoryMarshal.AsBytes(str.AsSpan()))
+ {
+ hash ^= b;
+ hash *= FNV_PRIME;
+ }
+
+ return (ushort)((hash >> 16) ^ (hash & ushort.MaxValue));
+ }
+
+ // stolen straight from a decompile of the mscorlib.dll shipped with Linux Hacknet
+ private static unsafe ushort MonoHash(string str)
+ {
+ fixed (char* ptr = str)
+ {
+ char* ptr2 = ptr;
+ char* ptr3 = (char*)((byte*)ptr2 + str.Length * 2 - 2);
+ int num = 0;
+ for (; ptr2 < ptr3; ptr2 += 2)
+ {
+ num = (num<<5) - num + *ptr2;
+ num = (num<<5) - num + ptr2[1];
+ }
+ ptr3++;
+ if (ptr2 < ptr3)
+ {
+ num = (num<<5) - num + *ptr2;
+ }
+ return (ushort)num;
+ }
+ }
+
+ // Both the 32 and 64 bit hashes are taken from this source https://referencesource.microsoft.com/#mscorlib/system/string.cs,898
+
+ private static unsafe ushort Fx32Hash(string str)
+ {
+ fixed (char* src = str)
+ {
+ int hash1 = (5381<<16) + 5381;
+ int hash2 = hash1;
+
+ int* pint = (int *)src;
+ int len = str.Length;
+ while (len > 2)
+ {
+ hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];
+ hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1];
+ pint += 2;
+ len -= 4;
+ }
+
+ if (len > 0)
+ {
+ hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];
+ }
+
+ return (ushort)(hash1 + (hash2 * 1566083941));
+ }
+ }
+
+ private static unsafe ushort Fx64Hash(string str)
+ {
+ fixed (char* src = str)
+ {
+ int hash1 = 5381;
+ int hash2 = hash1;
+
+ int c;
+ char* s = src;
+ while ((c = s[0]) != 0) {
+ hash1 = ((hash1 << 5) + hash1) ^ c;
+ c = s[1];
+ if (c == 0)
+ break;
+ hash2 = ((hash2 << 5) + hash2) ^ c;
+ s += 2;
+ }
+
+ return (ushort)(hash1 + (hash2 * 1566083941));
+ }
+ }
+
+ private static ushort GetHashCodeHashBad(string str) => (ushort)str.GetHashCode();
+}
diff --git a/PathfinderAPI/Util/XML/EventReader.cs b/PathfinderAPI/Util/XML/EventReader.cs
index 7aff3794..c6412a92 100644
--- a/PathfinderAPI/Util/XML/EventReader.cs
+++ b/PathfinderAPI/Util/XML/EventReader.cs
@@ -42,7 +42,7 @@ public void Parse()
if (Reader == null)
using (XmlReader reader = XmlReader.Create(new StringReader(Text))) while (reader.Read());
- using (Reader = Reader ?? XmlReader.Create(new StringReader(Text)))
+ using (Reader ??= XmlReader.Create(new StringReader(Text)))
{
while (Reader.Read())
{
diff --git a/PathfinderUpdater/Updater.cs b/PathfinderUpdater/Updater.cs
index 40abbe2c..43903332 100644
--- a/PathfinderUpdater/Updater.cs
+++ b/PathfinderUpdater/Updater.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.IO.Compression;
+using System.Net.Http;
using System.Net.Http.Headers;
using BepInEx.Hacknet;
using BepInEx.Logging;
diff --git a/README.md b/README.md
index 9dcd2849..9ed4543c 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
An extensive modding API and loader for Hacknet that enables practically limitless programable extensions to the game.
+Docs: https://arkhist.github.io/Hacknet-Pathfinder/
+
## Installation
There are several options available to choose to install Pathfinder, the installer .exe, the installer .py, or the manually with the .zip.