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,
};
}
}