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 Enabled; public readonly ConfigEntry RespectStoryEvents; public readonly ConfigEntry VanillaCityRule; private readonly Dictionary> m_cityToggles = new Dictionary>(); public ModSettings(ConfigFile config) { Enabled = config.Bind("General", "Enabled", true, "Master switch. When off, the caravanner behaves exactly like vanilla."); RespectStoryEvents = config.Bind("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("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("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 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 TravelDataList(MerchantFastTravel m) { return (List)s_travelDataList.GetValue(m); } public static List RegionIdList(MerchantFastTravel m) { return (List)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 list = GameApi.TravelDataList(__instance); List 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 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, }; } }