Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified 1.5/Assemblies/Hospitality.dll
Binary file not shown.
3 changes: 2 additions & 1 deletion Source/Source/JobGiver_BuyFood.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public override float GetPriority(Pawn pawn)

public override Job TryGiveJob(Pawn pawn)
{
if (pawn.needs.food == null) return null;
var canTakeFreeFood = pawn.GetMapComponent()?.guestsCanTakeFoodForFree ?? false;
if (pawn.needs.food == null || (pawn.GetMoney() == 0 && !canTakeFreeFood)) return null;

if (InternalDefOf.BuyFood.Worker.MissingRequiredCapacity(pawn) != null) return null;
//Log.Message($"{pawn.NameShortColored} is trying to buy food.");
Expand Down
78 changes: 41 additions & 37 deletions Source/Source/JoyGiver_BuyStuff.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Hospitality.Utilities;
Expand All @@ -14,7 +15,6 @@ public class JoyGiver_BuyStuff : JoyGiver
{
private readonly JobDef jobDefBrowse = DefDatabase<JobDef>.GetNamed("BrowseItems");
private readonly JobDef jobDefBuy = DefDatabase<JobDef>.GetNamed("BuyItem");
private readonly Dictionary<int, List<ulong>> recentlyLookedAt = new(); // Pawn ID, list of cell hashes
public JoyGiverDefShopping Def => (JoyGiverDefShopping)def;
protected virtual int OptimalMoneyForShopping => 50;

Expand All @@ -38,33 +38,32 @@ public override Job TryGiveJob(Pawn pawn)
{
var shoppingArea = pawn?.GetShoppingArea();
if (shoppingArea == null) return null;


// Gather all things lying on the ground, and in storage
var map = pawn.MapHeld;
var things = shoppingArea.ActiveCells.Where(cell => !HasRecentlyLookedAt(pawn, cell)).SelectMany(cell => map.thingGrid.ThingsListAtFast(cell))
.Where(t => t != null && ItemUtility.IsBuyableAtAll(pawn, t) && Qualifies(t, pawn)).ToList();
var storage = shoppingArea.ActiveCells.Where(cell => !HasRecentlyLookedAt(pawn, cell)).Select(cell => map.edificeGrid[cell]).OfType<Building_Storage>();
things.AddRange(storage.SelectMany(s => s.slotGroup.HeldThings.Where(t => ItemUtility.IsBuyableAtAll(pawn, t) && Qualifies(t, pawn))));
if (things.Count == 0) return null;
var groundThings = shoppingArea.ActiveCells.SelectMany(c => map.thingGrid.ThingsListAtFast(c));
var storedThings = shoppingArea.ActiveCells.Select(cell => map.edificeGrid[cell]).OfType<Building_Storage>().SelectMany(s => s.slotGroup.HeldThings);

var allThings = storedThings.Concat(groundThings).ToList();

var pawnWealth = pawn.GetMoney();
bool IsValidThing(Thing t) => ItemUtility.IsBuyableAtAll(pawn, pawnWealth, t) && Qualifies(t, pawn);

List<Thing> selectedThings = null;
var requiresFoodFactor = GuestUtility.GetRequiresFoodFactor(pawn);

// Try some things
IEnumerable<Thing> selectedThings;
if (requiresFoodFactor <= 0.8)
selectedThings = things.TakeRandom(5);
else
{
// Do not pick at random if the pawn needs food, select only food, and try to avoid disliked food.
selectedThings = things.Where(t => t.IsFood() && pawn.RaceProps.CanEverEat(t) && FoodUtility.MoodFromIngesting(pawn, t, t.def) >= 0);
if (selectedThings.FirstOrDefault() == null)
selectedThings = things.Where(t => t.IsFood() && pawn.RaceProps.CanEverEat(t));
selectedThings = selectedThings.ToList().TakeRandom(5);
// We can select non-food things, so anything random is good
selectedThings = SelectRandomThings(allThings, 5, IsValidThing);
}
var selection = selectedThings.Where(t => pawn.CanReach(t.Position, PathEndMode.Touch, Danger.None, false, false, TraverseMode.PassDoors)).ToArray();
foreach (var t in selection)
else
{
RegisterLookedAt(pawn, t.Position);
bool ValidFoodThing(Thing t) => t.IsFood() && pawn.RaceProps.CanEverEat(t) && FoodUtility.MoodFromIngesting(pawn, t, t.def) >= 0;
selectedThings = SelectRandomThings(allThings, 5, t => ValidFoodThing(t) && IsValidThing(t));
}


var selection = selectedThings.Where(t => pawn.CanReach(t.Position, PathEndMode.Touch, Danger.None, false, false, TraverseMode.PassDoors)).ToArray();

Thing thing = null;
if (selection.Length > 1)
thing = selection.MaxBy(t => Likey(pawn, t, requiresFoodFactor));
Expand Down Expand Up @@ -92,6 +91,27 @@ public override Job TryGiveJob(Pawn pawn)
return new Job(jobDefBuy, thing);
}

private static List<Thing> SelectRandomThings(List<Thing> things, int count, Func<Thing, bool> predicate)
{
var selectedThings = new List<Thing>();

// For performance ... doesn't necessarily mean we are actually doing this randomly :)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this mean they'll buy the same things all the time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - You can do something like randomize the index within the array of items - and wrap around if you reach the end (incrementing n times) if you would like that behaviour. I'd drop the second commit and re-write if you are interested, the first commit can be CP'd directly, imo.

Second commit is just an example of why this function is (still) slow after my first change, and offers a potential approach to making it faster. I don't think its a huge deal - but with large shopping areas, this causes the game to stutter noticeably.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what the idea behind the wrapping around is. I do understand the need for fixing the performance, though.
I find it quite important that they make a random selection. So if you could commit a fix for that, it'd be great. I suppose with this code it shouldn't be an issue if duplicate elements are picked by random, so that should allow for a fairly cheap solution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, you have some (semi-ordered) list things. And you want to select n random items from the list.

We want to avoid calling the IsValid check super frequently, and if we do - lets try to have it in semi-contiguous memory. To choose a random item, we can pick a random starting index, and increment from there, if our random index, is say - the second last index. We then wrap around to the start of the array again.

This ensures that (worst case) we still check every item in things, but in the best case - we can early out much faster and dramatically reduce the number of IsValid calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some fail safes? I feel if things is empty or less than 5, we might get issues. Or have you tested that?

var randomBaseIdx = Rand.Range(0, things.Count);
for (var i = 0; i < count; i++)
{
var thing = things[(randomBaseIdx + i) % things.Count];

if (predicate(thing))
{
selectedThings.Add(thing);
if (selectedThings.Count == count)
break;
}
}

return selectedThings;
}

private static float Likey(Pawn pawn, Thing thing, float requiresFoodFactor)
{
if (thing == null) return 0;
Expand Down Expand Up @@ -228,20 +248,4 @@ public static bool CanEat(Thing thing, Pawn pawn)
{
return thing.def.IsNutritionGivingIngestible && thing.def.IsWithinCategory(ThingCategoryDefOf.Foods) && ItemUtility.AlienFrameworkAllowsIt(pawn.def, thing.def, "CanEat");
}

private bool HasRecentlyLookedAt(Pawn pawn, IntVec3 cell)
Comment thread
OrionFive marked this conversation as resolved.
{
return recentlyLookedAt.TryGetValue(pawn.thingIDNumber, out var hashes) && hashes.Contains(cell.UniqueHashCode());
}

private void RegisterLookedAt(Pawn pawn, IntVec3 cell)
{
if (recentlyLookedAt.TryGetValue(pawn.thingIDNumber, out var hashes))
{
hashes.Add(cell.UniqueHashCode());
const int MaxCellsToRemember = 5;
if (hashes.Count > MaxCellsToRemember) hashes.RemoveAt(0);
}
else recentlyLookedAt.Add(pawn.thingIDNumber, [cell.UniqueHashCode()]);
}
}
12 changes: 9 additions & 3 deletions Source/Source/Utilities/ItemUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@

namespace Hospitality.Utilities;

[StaticConstructorOnStartup]
public static class ItemUtility
{
private static readonly Dictionary<string, MethodInfo> alienFrameworkMethods = new();

public static float priceFactor = 0.55f;
public static bool isCELoaded = false;

static ItemUtility()
{
isCELoaded = ModsConfig.ActiveModsInLoadOrder.Any(m => m.PackageId == "CETeam.CombatExtended");
}

public static void PocketHeadgear(this Pawn pawn)
{
Expand Down Expand Up @@ -150,7 +156,7 @@ public static bool AlienFrameworkAllowsIt(ThingDef raceDef, ThingDef thingDef, [
return method == null || (bool)method.Invoke(null, [thingDef, raceDef]);
}

public static bool IsBuyableAtAll(Pawn pawn, Thing thing)
public static bool IsBuyableAtAll(Pawn pawn, int pawnMoney, Thing thing)
{
if (thing.def.isUnfinishedThing) return false;

Expand All @@ -168,7 +174,7 @@ public static bool IsBuyableAtAll(Pawn pawn, Thing thing)
//}
var cost = Mathf.CeilToInt(GetPurchasingCost(thing));

if (cost > GetMoney(pawn))
if (cost > pawnMoney)
{
return false;
}
Expand Down Expand Up @@ -318,7 +324,7 @@ public static int GetInventorySpaceFor(this Pawn pawn, Thing current)

private static ThingComp GetInventory(this Pawn pawn)
{
return pawn.AllComps.FirstOrDefault(c => c.GetType().Name == "CompInventory");
return isCELoaded ? pawn.AllComps.FirstOrDefault(c => c.GetType().Name == "CompInventory") : null;
}

#endregion
Expand Down