Ludeon Forums

RimWorld => Mods => Topic started by: desert on July 06, 2017, 09:56:32 AM

Title: [Mod Idea] Re: Trade Beacon Limitations
Post by: desert on July 06, 2017, 09:56:32 AM
What could be done to allow full-map trade through a beacon or console, like with land-agent trade?

At the very least, for the existing beacon framework where would I go to change the beacon stockpile radius?
Title: Re: [Mod Idea] Re: Trade Beacon Limitations
Post by: Cryusaki on July 06, 2017, 11:18:51 AM
The variable you are looking for can be found in the games code for the trade beacon. Specifically it is called namespace RimWorld
{
public class Building_OrbitalTradeBeacon : Building
{
private const float TradeRadius = 7.9f;

Unfortunately you can't seem to get to it within XML editing otherwise this would be a simple patch mod.
Title: Re: [Mod Idea] Re: Trade Beacon Limitations
Post by: jamaicancastle on July 06, 2017, 04:04:38 PM
Just changing that variable won't be enough, because orbital trade beacons obey walls and doors, so it wouldn't be able to find things in buildings. However, if you're willing to make a C# mod, you should be able to find the part of the trading code where it collects items from the trade beacon, and simply tell it to use the ground trader item-finding code instead.
Title: Re: [Mod Idea] Re: Trade Beacon Limitations
Post by: desert on July 06, 2017, 11:46:57 PM
Quote from: jamaicancastle on July 06, 2017, 04:04:38 PM
Just changing that variable won't be enough, because orbital trade beacons obey walls and doors, so it wouldn't be able to find things in buildings. However, if you're willing to make a C# mod, you should be able to find the part of the trading code where it collects items from the trade beacon, and simply tell it to use the ground trader item-finding code instead.

Well alright, but I would need someone's help with this, given my code illiteracy.

Using the recommended decompiler ILSpy, I find the item (?) TradeUtility, which contains all the following code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Verse;
using Verse.AI.Group;

namespace RimWorld
{
public static class TradeUtility
{
public static bool EverTradeable(ThingDef def)
{
return def.tradeability != Tradeability.Never && ((def.category == ThingCategory.Item || def.category == ThingCategory.Pawn) && def.GetStatValueAbstract(StatDefOf.MarketValue, null) > 0f);
}

public static void SpawnDropPod(IntVec3 dropSpot, Map map, Thing t)
{
DropPodUtility.MakeDropPodAt(dropSpot, map, new ActiveDropPodInfo
{
SingleContainedThing = t,
leaveSlag = false
});
}

[DebuggerHidden]
public static IEnumerable<Thing> AllLaunchableThings(Map map)
{
HashSet<Thing> yieldedThings = new HashSet<Thing>();
foreach (Building_OrbitalTradeBeacon beacon in Building_OrbitalTradeBeacon.AllPowered(map))
{
foreach (IntVec3 c in beacon.TradeableCells)
{
List<Thing> thingList = c.GetThingList(map);
for (int i = 0; i < thingList.Count; i++)
{
Thing t = thingList[i];
if (TradeUtility.EverTradeable(t.def) && t.def.category == ThingCategory.Item && !yieldedThings.Contains(t) && TradeUtility.TradeableNow(t))
{
yieldedThings.Add(t);
yield return t;
}
}
}
}
}

[DebuggerHidden]
public static IEnumerable<Pawn> AllSellableColonyPawns(Map map)
{
foreach (Pawn p in map.mapPawns.PrisonersOfColonySpawned)
{
if (p.guest.PrisonerIsSecure)
{
yield return p;
}
}
foreach (Pawn p2 in map.mapPawns.SpawnedPawnsInFaction(Faction.OfPlayer))
{
if (p2.RaceProps.Animal && p2.HostFaction == null && !p2.InMentalState && !p2.Downed && map.mapTemperature.SeasonAndOutdoorTemperatureAcceptableFor(p2.def))
{
yield return p2;
}
}
}

public static Thing ThingFromStockToMergeWith(ITrader trader, Thing thing)
{
if (thing is Pawn)
{
return null;
}
foreach (Thing current in trader.Goods)
{
if (TransferableUtility.TransferAsOne(current, thing))
{
return current;
}
}
return null;
}

public static bool TradeableNow(Thing t)
{
return !t.IsNotFresh();
}

public static void LaunchThingsOfType(ThingDef resDef, int debt, Map map, TradeShip trader)
{
while (debt > 0)
{
Thing thing = null;
foreach (Building_OrbitalTradeBeacon current in Building_OrbitalTradeBeacon.AllPowered(map))
{
foreach (IntVec3 current2 in current.TradeableCells)
{
foreach (Thing current3 in map.thingGrid.ThingsAt(current2))
{
if (current3.def == resDef)
{
thing = current3;
goto IL_C6;
}
}
}
}
IL_C6:
if (thing == null)
{
Log.Error("Could not find any " + resDef + " to transfer to trader.");
break;
}
int num = Math.Min(debt, thing.stackCount);
if (trader != null)
{
trader.GiveSoldThingToTrader(thing, num, TradeSession.playerNegotiator);
}
else
{
thing.SplitOff(num).Destroy(DestroyMode.Vanish);
}
debt -= num;
}
}

public static void LaunchSilver(Map map, int fee)
{
TradeUtility.LaunchThingsOfType(ThingDefOf.Silver, fee, map, null);
}

public static Map PlayerHomeMapWithMostLaunchableSilver()
{
return (from x in Find.Maps
where x.IsPlayerHome
select x).MaxBy((Map x) => (from t in TradeUtility.AllLaunchableThings(x)
where t.def == ThingDefOf.Silver
select t).Sum((Thing t) => t.stackCount));
}

public static bool ColonyHasEnoughSilver(Map map, int fee)
{
return (from t in TradeUtility.AllLaunchableThings(map)
where t.def == ThingDefOf.Silver
select t).Sum((Thing t) => t.stackCount) >= fee;
}

public static void CheckInteractWithTradersTeachOpportunity(Pawn pawn)
{
if (pawn.Dead)
{
return;
}
Lord lord = pawn.GetLord();
if (lord != null && lord.CurLordToil is LordToil_DefendTraderCaravan)
{
LessonAutoActivator.TeachOpportunity(ConceptDefOf.InteractingWithTraders, pawn, OpportunityType.Important);
}
}
}
}



This element seems to enumerate things to trade for the UI if they are within the specified range of of the beacons.

public static IEnumerable<Thing> AllLaunchableThings(Map map)
{
HashSet<Thing> yieldedThings = new HashSet<Thing>();
foreach (Building_OrbitalTradeBeacon beacon in Building_OrbitalTradeBeacon.AllPowered(map))
{
foreach (IntVec3 c in beacon.TradeableCells)
{
List<Thing> thingList = c.GetThingList(map);
for (int i = 0; i < thingList.Count; i++)
{
Thing t = thingList[i];
if (TradeUtility.EverTradeable(t.def) && t.def.category == ThingCategory.Item && !yieldedThings.Contains(t) && TradeUtility.TradeableNow(t))
{
yieldedThings.Add(t);
yield return t;
}
}
}
}
}


I don't know how to find the code for caravan/land trade. What would I do here?

Alternatively, the item Building_Orbital Trade Beacon contains the code Cryusaki posted; the "7.9f" figure appears twice. How would I safely change the effective radius here if that were the plan?

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using UnityEngine;
using Verse;

namespace RimWorld
{
public class Building_OrbitalTradeBeacon : Building
{
private const float TradeRadius = 7.9f;

private static List<IntVec3> tradeableCells = new List<IntVec3>();

public IEnumerable<IntVec3> TradeableCells
{
get
{
return Building_OrbitalTradeBeacon.TradeableCellsAround(base.Position, base.Map);
}
}

[DebuggerHidden]
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo g in base.GetGizmos())
{
yield return g;
}
if (DesignatorUtility.FindAllowedDesignator<Designator_ZoneAddStockpile_Resources>() != null)
{
yield return new Command_Action
{
action = new Action(this.MakeMatchingStockpile),
hotKey = KeyBindingDefOf.Misc1,
defaultDesc = "CommandMakeBeaconStockpileDesc".Translate(),
icon = ContentFinder<Texture2D>.Get("UI/Designators/ZoneCreate_Stockpile", true),
defaultLabel = "CommandMakeBeaconStockpileLabel".Translate()
};
}
}

private void MakeMatchingStockpile()
{
Designator des = DesignatorUtility.FindAllowedDesignator<Designator_ZoneAddStockpile_Resources>();
des.DesignateMultiCell(from c in this.TradeableCells
where des.CanDesignateCell(c).Accepted
select c);
}

public static List<IntVec3> TradeableCellsAround(IntVec3 pos, Map map)
{
Building_OrbitalTradeBeacon.tradeableCells.Clear();
if (!pos.InBounds(map))
{
return Building_OrbitalTradeBeacon.tradeableCells;
}
Region region = pos.GetRegion(map, RegionType.Set_Passable);
if (region == null)
{
return Building_OrbitalTradeBeacon.tradeableCells;
}
RegionTraverser.BreadthFirstTraverse(region, (Region from, Region r) => r.portal == null, delegate(Region r)
{
foreach (IntVec3 current in r.Cells)
{
if (current.InHorDistOf(pos, 7.9f))
{
Building_OrbitalTradeBeacon.tradeableCells.Add(current);
}
}
return false;
}, 13, RegionType.Set_Passable);
return Building_OrbitalTradeBeacon.tradeableCells;
}

[DebuggerHidden]
public static IEnumerable<Building_OrbitalTradeBeacon> AllPowered(Map map)
{
foreach (Building_OrbitalTradeBeacon b in map.listerBuildings.AllBuildingsColonistOfClass<Building_OrbitalTradeBeacon>())
{
CompPowerTrader power = b.GetComp<CompPowerTrader>();
if (power == null || power.PowerOn)
{
yield return b;
}
}
}
}
}

Title: Re: [Mod Idea] Re: Trade Beacon Limitations
Post by: jamaicancastle on July 07, 2017, 03:28:31 AM
So I tracked down the code that NPC travelers/caravans appear to use for their trading, which is in the Pawn_TraderTracker class. For reasons totally inexplicable to me, it has a completely different structure to it that I don't think can be neatly fitted into the orbital trade ship code. So let's look at the beacon:

The relevant function there, as you can probably guess, is TradeableCellsAround. As you noted, it seems to have hardcoded the 7.9f value for its radius rather than actually referring to the constant, in a way guaranteed to annoy your CS professor. (Probably an oversight.) The only one that actually does anything is the one in the function; the constant, as far as I can tell, isn't actually referred to anywhere.

Anyway, you can modify that to turn it up to 50 or 100 or 1,000 or whatever else you want (just remember that it expects a float: "100f"), but as I mentioned above, that's not the whole story.

RegionTraverser.BreadthFirstTraverse(region, [some other unimportant parameters], 13, RegionType.Set_Passable);
This is part of the TradeableCellsAround function; it's the bit that, after checking for various error cases, actually populates the list of cells. (One of the parts I elided is where the 7.9f radius is established, incidentally, in a very inside-out bit of code that makes my head ache.) In this case is uses regions, which are bits of the map that the game uses to build things like rooms, temperature variations, and whatnot. The key point is the RegionType.Set_Passable. This means it will only attempt to crawl regions that are fully passable (not walls or doors) in order to find what it's looking for. The 13, incidentally, is related to the depth with which it will search, although in a way that's not totally clear to me. (Hey, I didn't take Algorithms as an undergrad. Maybe should've.)

So basically, you would have the following options:
- Just change the 7.9f in TradeableCellsAround to another value, say 100.0f. It would still be blocked by doors, but you could put one outside and have all the storage space you could practically need (it doesn't care about roofs, so put up a roofed but unwalled space off to one side for perishables). In practice, depending on how Rimworld sets up regions, you might or might not need to experiment with replacing the 13 above it with a higher number.
- You could change the beacon to search impassable as well as passable terrain with a huge radius, but there's really not much point, because you're better off with this:
- Cut right to the chase and just give it all the cells in the map. Delete everything after the first "if" block to the end of the function, and replace it with:
return map.AllCells.ToList();

There is one caveat with this method: I have no idea how much lag it will produce. I assume some. This would manifest at three times: when placing an OTB*, when one is first built, and when a trade ship actually arrives (it shouldn't-but-might also show up when you sell item(s)). Also, absolutely don't touch the "create stockpile" button with an OTB with this mod; it'll try to make your entire map into a stockpile with, I imagine, hilarious results.

* You might or might not be able to solve this by removing the placeWorkers block from the OTB's XML. This will stop it trying to show the radius of the OTB while placing (or selecting) it, which is just as well since obviously it's just going to highlight the whole map.

Unfortunately, when it comes to actually putting these insights into practice with a mod, I am something of a beginner as well, so I would be grateful if one of our other regulars could explain that bit? (That is, given specific changes that have to happen to this one existing function, what's the best way to patch it?)
Title: Re: [Mod Idea] Re: Trade Beacon Limitations
Post by: desert on July 07, 2017, 07:05:21 AM
Quote from: jamaicancastle on July 07, 2017, 03:28:31 AM
So I tracked down the code that NPC travelers/caravans appear to use for their trading, which is in the Pawn_TraderTracker class. For reasons totally inexplicable to me, it has a completely different structure to it that I don't think can be neatly fitted into the orbital trade ship code. So let's look at the beacon:

The relevant function there, as you can probably guess, is TradeableCellsAround. As you noted, it seems to have hardcoded the 7.9f value for its radius rather than actually referring to the constant, in a way guaranteed to annoy your CS professor. (Probably an oversight.) The only one that actually does anything is the one in the function; the constant, as far as I can tell, isn't actually referred to anywhere.

Anyway, you can modify that to turn it up to 50 or 100 or 1,000 or whatever else you want (just remember that it expects a float: "100f"), but as I mentioned above, that's not the whole story.

RegionTraverser.BreadthFirstTraverse(region, [some other unimportant parameters], 13, RegionType.Set_Passable);
This is part of the TradeableCellsAround function; it's the bit that, after checking for various error cases, actually populates the list of cells. (One of the parts I elided is where the 7.9f radius is established, incidentally, in a very inside-out bit of code that makes my head ache.) In this case is uses regions, which are bits of the map that the game uses to build things like rooms, temperature variations, and whatnot. The key point is the RegionType.Set_Passable. This means it will only attempt to crawl regions that are fully passable (not walls or doors) in order to find what it's looking for. The 13, incidentally, is related to the depth with which it will search, although in a way that's not totally clear to me. (Hey, I didn't take Algorithms as an undergrad. Maybe should've.)

So basically, you would have the following options:
- Just change the 7.9f in TradeableCellsAround to another value, say 100.0f. It would still be blocked by doors, but you could put one outside and have all the storage space you could practically need (it doesn't care about roofs, so put up a roofed but unwalled space off to one side for perishables). In practice, depending on how Rimworld sets up regions, you might or might not need to experiment with replacing the 13 above it with a higher number.
- You could change the beacon to search impassable as well as passable terrain with a huge radius, but there's really not much point, because you're better off with this:
- Cut right to the chase and just give it all the cells in the map. Delete everything after the first "if" block to the end of the function, and replace it with:
return map.AllCells.ToList();

There is one caveat with this method: I have no idea how much lag it will produce. I assume some. This would manifest at three times: when placing an OTB*, when one is first built, and when a trade ship actually arrives (it shouldn't-but-might also show up when you sell item(s)). Also, absolutely don't touch the "create stockpile" button with an OTB with this mod; it'll try to make your entire map into a stockpile with, I imagine, hilarious results.

* You might or might not be able to solve this by removing the placeWorkers block from the OTB's XML. This will stop it trying to show the radius of the OTB while placing (or selecting) it, which is just as well since obviously it's just going to highlight the whole map.

Unfortunately, when it comes to actually putting these insights into practice with a mod, I am something of a beginner as well, so I would be grateful if one of our other regulars could explain that bit? (That is, given specific changes that have to happen to this one existing function, what's the best way to patch it?)

Excellent work. So, toward a workable mod that sets a beacon to consider the whole map*, we have three elements:

1. Replace the TradeableCellsAround function in the object(?) Building_OrbitalTradeBeacon with code enabling full-map coverage for the trade beacon.
public static List<IntVec3> TradeableCellsAround(IntVec3 pos, Map map)
{
Building_OrbitalTradeBeacon.tradeableCells.Clear();
if (!pos.InBounds(map))
{
return Building_OrbitalTradeBeacon.tradeableCells;
}
return map.AllCells.ToList();
}


2. Supplementary edit to the structure XML removing the building radius highlight.
3. What is the actual mod to be implemented? The original source code is being overridden, so how does that proceed?

Would it be technically simpler to duplicate the OTB, create a new structure that duplicates the functions, but with the desired changes? Call it OTB 2.0...

*I imagine with this proposed mod you would never need more than one beacon, but would building more than one with the new functionality create interference or other problems?
Title: Re: [Mod Idea] Re: Trade Beacon Limitations
Post by: desert on July 09, 2017, 03:56:05 PM
Any thoughts? Here are the options as determined:

1.  Replace the TradeableCellsAround function in the object(?) Building_OrbitalTradeBeacon with code enabling full-map coverage for the trade beacon. Supplementary edit to the structure XML removing the building radius highlight.
public static List<IntVec3> TradeableCellsAround(IntVec3 pos, Map map)
{
Building_OrbitalTradeBeacon.tradeableCells.Clear();
if (!pos.InBounds(map))
{
return Building_OrbitalTradeBeacon.tradeableCells;
}
return map.AllCells.ToList();
}

2. To avoid having to mod the source code, create a simpler mod that duplicates the Orbital Trade Beacon with above changed functionality, but as a distinct construction from the base-game OTB.
Title: Re: [Mod Idea] Re: Trade Beacon Limitations
Post by: Transcend on February 10, 2018, 05:04:24 AM
Created a mod for this: https://ludeon.com/forums/index.php?topic=39023.0 (https://ludeon.com/forums/index.php?topic=39023.0)