Decompiled source of CaravannerUnbound v1.0.2

plugins/CaravannerUnbound/CaravannerUnbound.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;

[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace CaravannerUnbound;

[BepInPlugin("com.spencer4792.caravannerunbound", "CaravannerUnbound", "1.0.2")]
public class CaravannerUnboundPlugin : BaseUnityPlugin
{
	public const string GUID = "com.spencer4792.caravannerunbound";

	public const string NAME = "CaravannerUnbound";

	public const string VERSION = "1.0.2";

	internal static ManualLogSource Log;

	internal static ModSettings Settings;

	internal void Awake()
	{
		//IL_003a: Unknown result type (might be due to invalid IL or missing references)
		Log = ((BaseUnityPlugin)this).Logger;
		Settings = new ModSettings(((BaseUnityPlugin)this).Config);
		if (!GameApi.Initialize())
		{
			Log.LogError((object)"Game API validation failed — see warnings above. Mod disabled.");
			return;
		}
		new Harmony("com.spencer4792.caravannerunbound").PatchAll(typeof(DestinationExpander));
		Log.LogMessage((object)string.Format("{0} {1} ready.", "CaravannerUnbound", "1.0.2"));
	}
}
internal class ModSettings
{
	public readonly ConfigEntry<bool> Enabled;

	public readonly ConfigEntry<bool> RespectStoryEvents;

	public readonly ConfigEntry<bool> VanillaCityRule;

	private readonly Dictionary<City, ConfigEntry<bool>> m_cityToggles = new Dictionary<City, ConfigEntry<bool>>();

	public ModSettings(ConfigFile config)
	{
		Enabled = config.Bind<bool>("General", "Enabled", true, "Master switch. When off, the caravanner behaves exactly like vanilla.");
		RespectStoryEvents = config.Bind<bool>("General", "RespectStoryEvents", true, "Keep destinations consistent with your story: destroyed cities stay unavailable, New Sirocco appears only once the caravan trader has set up there.");
		VanillaCityRule = config.Bind<bool>("General", "OnlyOfferTravelInCities", false, "Restore the vanilla rule that the full destination list is only offered while inside a city. Off = travel from anywhere.");
		foreach (City value in Enum.GetValues(typeof(City)))
		{
			m_cityToggles[value] = config.Bind<bool>("Destinations", value.ToString(), true, $"Offer {value} as a travel destination.");
		}
	}

	public bool IsDestinationWanted(City city)
	{
		return m_cityToggles[city].Value;
	}
}
internal enum City
{
	Cierzo = 100,
	Monsoon = 200,
	Levant = 300,
	Harmattan = 400,
	Berg = 500,
	NewSirocco = 601
}
internal static class GameApi
{
	private static FieldInfo s_travelDataList;

	private static FieldInfo s_regionIdList;

	private static FieldInfo s_originCity;

	private static MethodInfo s_levantAccessible;

	public static string CierzoDestroyedEvent { get; private set; }

	public static string BergExpulsedEvent { get; private set; }

	public static string SiroccoTraderEvent { get; private set; }

	public static bool Initialize()
	{
		Type typeFromHandle = typeof(MerchantFastTravel);
		s_travelDataList = Find(typeFromHandle.GetField("tmpCurrentTravelData", AccessTools.all), "tmpCurrentTravelData");
		s_regionIdList = Find(typeFromHandle.GetField("m_travelToRegion", AccessTools.all), "m_travelToRegion");
		s_originCity = Find(typeFromHandle.GetField("m_currentAreaAssociatedCity", AccessTools.all), "m_currentAreaAssociatedCity");
		s_levantAccessible = Find(typeFromHandle.GetMethod("CheckIfLevantBlocked", AccessTools.all), "CheckIfLevantBlocked");
		CierzoDestroyedEvent = ReadGameConstant(typeFromHandle, "CIERZODESTROYED_EVENTID", "lDHL_XMS7kKEs0uOqrLQjw");
		BergExpulsedEvent = ReadGameConstant(typeFromHandle, "BERGEXPULSED_EVENTID", "vW4sarzBGkalTwy_KhGI6A");
		SiroccoTraderEvent = ReadGameConstant(typeFromHandle, "CARAVAN_TRADER_IN_NEWSIROCCO_EVENT_ID", "eYmZGb_BJ0qAtpcwrndhTg");
		return s_travelDataList != null && s_regionIdList != null && s_originCity != null && s_levantAccessible != null;
	}

	private static T Find<T>(T member, string name) where T : class
	{
		if (member == null)
		{
			CaravannerUnboundPlugin.Log.LogWarning((object)$"MerchantFastTravel.{name} not found in this game version.");
		}
		return member;
	}

	private static string ReadGameConstant(Type type, string constName, string fallback)
	{
		try
		{
			string text = type.GetField(constName, AccessTools.all)?.GetRawConstantValue() as string;
			if (!string.IsNullOrEmpty(text))
			{
				return text;
			}
		}
		catch
		{
		}
		CaravannerUnboundPlugin.Log.LogWarning((object)$"Could not read {constName} from the game; using known value.");
		return fallback;
	}

	public static List<TravelData> TravelDataList(MerchantFastTravel m)
	{
		return (List<TravelData>)s_travelDataList.GetValue(m);
	}

	public static List<int> RegionIdList(MerchantFastTravel m)
	{
		return (List<int>)s_regionIdList.GetValue(m);
	}

	public static City OriginCity(MerchantFastTravel m)
	{
		return (City)(int)s_originCity.GetValue(m);
	}

	public static bool LevantAccessible(MerchantFastTravel m)
	{
		return (bool)s_levantAccessible.Invoke(m, null);
	}
}
[HarmonyPatch(typeof(MerchantFastTravel), "RefreshTravelDestinations", new Type[] { typeof(bool) })]
internal static class DestinationExpander
{
	[HarmonyPostfix]
	private static void ExpandDestinations(MerchantFastTravel __instance, bool _forceRefresh)
	{
		try
		{
			ModSettings settings = CaravannerUnboundPlugin.Settings;
			if (!settings.Enabled.Value || (PhotonNetwork.isNonMasterClientInRoom && !_forceRefresh) || (settings.VanillaCityRule.Value && !AreaManager.Instance.GetIsCurrentAreaTownOrCity()))
			{
				return;
			}
			List<TravelData> list = GameApi.TravelDataList(__instance);
			List<int> list2 = GameApi.RegionIdList(__instance);
			City city = GameApi.OriginCity(__instance);
			int num = 0;
			foreach (City value in Enum.GetValues(typeof(City)))
			{
				if (value == city || AlreadyOffered(list, value) || !settings.IsDestinationWanted(value) || !StoryAllows(__instance, value))
				{
					continue;
				}
				TravelData merchantTravelData = AreaManager.Instance.GetMerchantTravelData((AreaEnum)city, (AreaEnum)value);
				if (merchantTravelData != null)
				{
					if (!list2.Contains((int)value))
					{
						list2.Add((int)value);
					}
					list.Add(merchantTravelData);
					num++;
				}
			}
			if (settings.IsDestinationWanted(City.Harmattan) && city != City.Harmattan)
			{
				MerchantFastTravel.CanTravelToHarmattan = true;
			}
			if (num > 0)
			{
				MerchantFastTravel.SyncTravelDestinations();
			}
		}
		catch (Exception arg)
		{
			CaravannerUnboundPlugin.Log.LogError((object)$"ExpandDestinations: {arg}");
		}
	}

	private static bool AlreadyOffered(List<TravelData> offered, City city)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Unknown result type (might be due to invalid IL or missing references)
		AreaEnum val = (AreaEnum)city;
		for (int i = 0; i < offered.Count; i++)
		{
			if (offered[i] != null && offered[i].Destination == val)
			{
				return true;
			}
		}
		return false;
	}

	private static bool StoryAllows(MerchantFastTravel instance, City city)
	{
		if (!CaravannerUnboundPlugin.Settings.RespectStoryEvents.Value)
		{
			return true;
		}
		return city switch
		{
			City.Cierzo => !QuestEventManager.Instance.HasQuestEvent(GameApi.CierzoDestroyedEvent), 
			City.Berg => !QuestEventManager.Instance.HasQuestEvent(GameApi.BergExpulsedEvent), 
			City.Levant => GameApi.LevantAccessible(instance), 
			City.NewSirocco => QuestEventManager.Instance.HasQuestEvent(GameApi.SiroccoTraderEvent), 
			_ => true, 
		};
	}
}