Bundling with Foxybot



April 13th, 2017 by Diana Coman

The ever useful Foxybot got shot a bit in one of its many legs at the last euloran update - previously fixed and reliable lists of ingredients turned all flighty and fickle, with ingredient numbers changing even as one crafts. As a result, automated craft runs can turn into a bit of a pain to both plan for (how many ingredients does one even NEED if each click potentially requires a different amount??) and to execute flawlessly and tirelessly over days and weeks, all foxybot-style. Still, people found a way around this trouble: just bundle stuff upfront and then ...craft it, build it, eat it or whatever else you want to do to it, changing blueprint or not. And if bundling is the new planning and the new pre-requisite to successful crafting for a useful bot, let's teach Foxybot to bundle, shall we?

You'll need all of half an hour time and a bit of thinking - hopefully that's something you *can* find around your house if not about your person at any given time. Grab your current version of the Foxybot and open its botactivity.h file. We'll make bundling a new activity for the bot so simply plonk in there the declaration of this new activity, similar to the other ones you'll see in the file. Since code is really the place for precision, not imaginary friends and flourishes, we'll call it simply:

class BundleActivity: public BotActivity, iTimerEvent
{
};

The above lines declared the new BundleActiviy as a refined version of the generic BotActivity and of iTimerEvent. Basically so far it just brings those two together and it does nothing more than that. Clearly that's not enough, so let's add some useful methods and fields between those two curly brackets where the body of our new activity is to be defined:

public:
	BundleActivity();	//constructor
	~BundleActivity();	//destructor
	void HandleMessage(MsgEntry* message );	//this will react when server says something we care about!
	void ScheduleNewPerform(int delay);	//this is like an alarm clock - wake up lazy bones and do the next step
	bool Perform(iTimerEvent *ev);		//this IS the next step, called each time the alarm clock rings

	//for iBase			//those are the price of using crystal space; don't get me started.
	void IncRef(){}
	void DecRef(){}
	int GetRefCount() {return 0;}
	void* QueryInterface(scfInterfaceID i, int a) {}
	void AddRefOwner(void **, CS::Threading::Mutex*) {}
	void RemoveRefOwner(void**){}
	scfInterfaceMetadataList* GetInterfaceMetadata() {return NULL;}

protected:
	void StartWork();	//this is starting point, gotta start from somewhere, no?
	void WrapUp();		//at the end of each cycle one has to tidy up and this is just the code to do it
	void DoInit();		//at the start of each cycle one has to get all tools out and get everything ready

private:
  recipe rcp;		//this is the blueprint saying WHAT and HOW we are to bundle; can't do without it
  bool bundleReady;	//logic here is so simple that we can get away with just a few flags: is bundle ready?
  bool recipeReady;	//is recipe ready/read?
  bool bundlingStarted;	//has bundling even started?
  int containerID;	//id of container used for mixing; needed to filter view container messages for the correct one
  EID containerEID;	//this is Planeshit idea of ids: making at least 2 for each thing and several more to spare.
  int percent;	//used for choosing number of ingredients	- if only it would be percent...

Save botactivity.h and close it. Open botactivity.cpp and let's now actually write all those things we just promised in the .h file. Ain't no code working on empty promises and fancy headers, you know? So there we go, plonk in botactivity.cpp:

BundleActivity::BundleActivity()
{
  name = csString("bundle");		//we need this to react to /bot bundle
  textcolour = TEXT_COLOUR;		//what colour foxybot speaks in for this activity.
}

BundleActivity::~BundleActivity()
{//nothing to destruct really!
}

void BundleActivity::HandleMessage(MsgEntry* message )
{
	if (!IsOngoing())	//if I'm not working, then I don't care what server says!
	  return;
	//only if we are actually looking for bundle
	if (!bundleReady && message->GetType() == MSGTYPE_VIEW_CONTAINER)
	{//did things change in that container?
		psViewContainerDescription mesg(message, psengine->GetNetManager()->GetConnection()->GetAccessPointers());
		if (mesg.containerID == containerID)	//this is for the container we are working with, check if there is a bundle
		{//kind of lulzy if we were to look in the WRONG container, so check id, yeah

			if (mesg.contents.GetSize()>0 && mesg.contents[0].name.Find("Bundle") < mesg.contents[0].name.Length()) //it is a bundle {
                                    bundleReady = true; //hey, we've got a bundle, we can go ahead!
                        }
                }
        }
        else if (!recipeReady && message->GetType() == MSGTYPE_CRAFT_INFO)
	{	//this is when we need to read the recipe/blueprint that is equipped
		psMsgCraftingInfo incoming(message);
		csString itemDescription(incoming.craftInfo);

		rcp.ParseRecipe(itemDescription, percent>50);
		worldHandler::CloseOpenWindow(csString("Read Book"));
		OutputMsg(csString("Done reading recipe."));
		recipeReady = true;
	}
}

void BundleActivity::ScheduleNewPerform(int delay)
{
	csRef<iEventTimer> timer = worldHandler::GetStandardTimer();
	timer->AddTimerEvent(this, delay);	//set alarm clock
}

bool BundleActivity::Perform(iTimerEvent *ev)
{
	if (!IsOngoing() && !IsFinished())	//if I ain't working, I ain't performing.
		return false;

	if (timesDone >= timesToRepeat)		//done is done
	{	//nothing more to do here.
		Finish();
		return false;	//this means DON'T set up that alarm anymore!
	}

	if (!recipeReady)	//reading comprehension trouble? Just ask to read again. And again and ...
	{
		//ask for it again and wait another round
		psViewItemDescription out(CONTAINER_INVENTORY_EQUIPMENT, PSCHARACTER_SLOT_MIND);
		out.SendMessage();
		return true;	//re-schedule
	}
	else 	//recipeReady - we READ it, hooray
		if (!bundlingStarted)	//if it's not started, then...we start it now; simple!
		{

			OutputMsg(csString("Bundling action combining..."));
			char out[200];
			sprintf(out, "Done %d items, %d left to do.", timesDone, timesToRepeat-timesDone);
			OutputMsg(csString(out));

			if (!worldHandler::TargetEID(containerEID))
			{
				OutputMsg(csString("Could not find container within reach!"));
				Error();
				return false;
			}

			if (!worldHandler::OpenTarget())
			{
				OutputMsg(csString("Could not open the container!"));
				Error();
				return false;
			}
			OutputMsg(csString("Moving ingredients to container for bundling"));
			//move ingredients from inventory to container
			//first check for any bundles

			int toContainer = containerEID.Unbox();

			csHash<int, csString>::GlobalIterator iterIngredients(rcp.GetIngredientsList()->GetIterator());

			int nextEmptySlot = 0;
			if (!iterIngredients.HasNext())
			{
				OutputMsg(csString("Empty ingredients list!"));
				Error();
				return false;
			}
			while (iterIngredients.HasNext())
			{
				csString itemName;
				int quantity = iterIngredients.Next(itemName);

				char out[1000];
				sprintf(out, "Ingredient %s: %d", itemName.GetData(), quantity);
				OutputMsg(csString(out));

				psInventoryCache::CachedItemDescription* from = worldHandler::FindItemSlot(itemName, false);
				if (!from || from->stackCount < quantity) {
                                    OutputMsg(csString("Not enough ingredients for bundling! Bot stopping."));
                                    Error();
                                    return false;
                               } else {
                                    worldHandler::MoveItems(from->containerID, from->slot, toContainer, nextEmptySlot, quantity);
				}
				nextEmptySlot = nextEmptySlot + 1;
			}
			OutputMsg(csString("Done with ingredients. Starting to combine."));

			worldHandler::CombineContentsInTarget();

			bundlingStarted = true;
			return true;	//re-schedule
		}
		else if (!bundleReady)	//we know bundling has started but..no bundle yet
		{
			if (csGetTicks() - startTime >= timeout)	//we have SOME patience; but no more than timeout
			{
				OutputMsg("Timedout, moving on.");
				//take items, ask for unstick + start over
				worldHandler::TakeAllFromTarget();
				worldHandler::ExecCmd(csString("/unstick"));	//this is so that ongoing actions are cancelled as otherwise everything is jammed afterwards
				DoInit();	//start all over again;
			}
			else
			{//maybe it IS done, but we...missed the memo; ask for it again
				//ask for container contents, maybe this got lost somehow
				worldHandler::OpenTargetEID(containerEID);
			}
			return true;	//simply reschedule here anyway, either way
		}
		else //all of them are true, so we're done, grab bundle + start again
		{
			//take items
			worldHandler::TakeAllFromTarget();
			Finish();
			if (timesDone < timesToRepeat) //if we are not done, go ahead
                        {
                          DoInit();
                          return true;
                        }
                        else return false; //done, yay
                }
}

void BundleActivity::StartWork()
{
  OutputMsg(csString("Bundle activity started."));
  ScheduleNewPerform(MIN_DELAY);
}

void BundleActivity::WrapUp()
{
  bundleReady = false; //at the end of a step, bundle is not ready!
  char msg[1000];
  sprintf(msg, "%d bundles done, %d potential bundles left.", timesDone, timesToRepeat-timesDone);
  OutputMsg(csString(msg));
}

void BundleActivity::DoInit()
{
  bundleReady = false;
  recipeReady = false;
  bundlingStarted = false;
  if (timesDone == 0)
  { //get parameter if any
    WordArray words(cmd,false);
    if (words.GetCount()>0)	//it has a percent, so let's get this; default it is 100%
	percent = words.GetInt(0);
    else percent = 100;	//default

   //get setup
   csString recipeName = worldHandler::GetBrainItemName();
   if (recipeName.CompareNoCase(""))
   {
      Error();
      OutputMsg(csString("No blueprint equipped, please equip the bp for what you want to bundle and /bot bundle again."));
      return;
    }
    else
	rcp.setRecipeName(recipeName);

  containerEID = worldHandler::GetTargetEID();
  if (containerEID == -1)
  {
	Error();
	//actionToDo = CRAFTING_NONE;
	OutputMsg(csString("Kindly target (open) the container you want used for this craft run and then /bot craft again."));
        return;
  }
  containerID = containerEID.Unbox();
}

  //read recipe = EVERY TIME!
  psViewItemDescription out(CONTAINER_INVENTORY_EQUIPMENT, PSCHARACTER_SLOT_MIND);
  out.SendMessage();
  OutputMsg(csString("Reading the recipe..."));
}

Now we only need to actually add this new activiy to Foxybot so that we can call it. Save botactivity.cpp and close it. Open foxybot.cpp and add the following line in the constructor below the other similar ones (it's line 40 in my file):

activities[3] = new BundleActivity();	//adding the new activity to foxybot's "can do" list

Notice how Foxybot will know to delete it safely anyway since it deletes ALL the activities in its list in the destructor. But for that to work well we still need to let it know we increased the number of its activities. So save foxybot.cpp and then open foxybot.h and change:

static const int activitiesCount = 3;

...to:

static const int activitiesCount = 4;

That's it. Save foxybot.h and close it. Then simply re-compile the whole thing:

jam -aq client

Get yourself a drink while the thing compiles and then launch the client again. Simply drop a table or something, equip the bp you want, target (open/examine) the table and type:

/bot bundle 2000

Bot should start work on making 2000 bundles of whatever you have equipped in your character's mind. Drink that drink fast, for it won't take long!

Comments feed: RSS 2.0

8 Responses to “Bundling with Foxybot”

  1. Which quantity does it use in cases where there is a range? Min? Max? And what is the correct way to allow users to specify specific quantities per ingredient? For example: my toils take 3 to 5 waters and some range of numina; I prefer to use the minimum waters and the maximum numina. (since water is a lot harder to make right now, and numina i have too much of) But min/max isn't ideal either since the max numina might be wasteful and some simple math shows i can get the same bundle quality by using a few less than the max numina.

    The problem with implimenting this is: every blueprint has a different number of ingredients, which leads me to think that I can't simply add a parameter to the /bot bundle for each ingredient. Perhaps just one parameter that is a comma separated list of integers? Is there a reason not to do this? Besides it will look kinda ugly.

  2. Diana Coman says:

    By default it uses max. At the moment the recipe class works only with either min or max, but this activity is meant to allow percentage as parameter so it accepts a number. To make it work on the other end, the recipe class needs to be updated/improved.

    Considering what you want to do, I think the best approach might be to give it a filename as a parameter and it can read from there any specific combinations you want. The advantage of this being that you can make your file(s) and reuse without having to type all the things every time. However, there is of course no trouble at all in giving it any number of parameters (even unknown at code-writing time) and then use those, sure.

  3. Diana Coman says:

    Feedback from users: if your bot ends up building only *one* single bundle without proceeding further, you might be missing the subscription to the sort of message that the bot uses to figure out when the bundle is done. To fix this:
    - check in foxybot.cpp if you have " psengine->GetMsgHandler()->Subscribe(this, MSGTYPE_VIEW_CONTAINER);" It should be in foxyBot::PostSetup() where it subscribes to all sort of messages of interest.
    If you don't have that, add it!

  4. wyrob pieczatek, says:

    bookmarked!!, I really like your web site!

  5. Diana Coman says:

    Hopefully my website really likes bookmarks, too.

  6. […] table in question is not any table but the very useful and extremely versatile craft-table that Foxybot uses to carry stuff around when one doesn’t use it to craft or to store or to expand […]

  7. […] thing that seems to be rather wanted and in short supply at the moment). So noob was set with the bundling bot to make a few hundred bundles that came out an actually reasonable ~1300 quality because […]

Leave a Reply