Bonzi Buddy Remake
Table of contents
Introduction
A few months ago I started a small task of trying to port a spyware-free version of Bonzi Buddy (the little purple gorilla desktop buddy from the 2000s). During conception, I discovered a great library called Double Agent. This library essentially ported the old Microsoft Agent framework (the tech behind Clippy and Peedy the Parrot) to more modern platforms. Using this, I was able to essentially program my own version of Bonzi Buddy using his .acs file I found somewhere online.
The above is a list of features I am implementing in my first version of Bonzi Buddy (minus the voice commands, for now). I've decided to program Bonzi in C# (the other supported languages of Double Agent are VB, C++, and JS(?)). Most of these features utilize API calls to get relevant data back. Initially, I had 'Jokes', 'Trivia' and 'Facts' hard-coded into the program, but this was cumbersome and needlessly time-consuming, so I opted to use free APIs to get the job done.
How It Works?
DoubleAgent
DoubleAgent is a library that essentially emulates Microsoft Agent to modern platforms. A lot of the API is synonymous with Microsoft Agent. DoubleAgent needs to be installed on the user's machine for this program to work, so I am hoping to resolve dependencies in Bonzi Buddy's first or second initial release.
I am planning to make a tutorial on using DoubleAgent in the near future, as finding proper documentation/tutorials proved quite cumbersome, and want to create an easy-to-use guide for anyone interested in creating their own desktop buddies.
Environment
Bonzi Buddy is built using .NET 6, C# and WinForms. Initially, I had tried to use WPF, but wouldn't work with DoubleAgent. I was able to get it running using a simple Console app and WinForms so decided to go with WinForms.
I am programming entirely in MS Visual Studio 2022 over Rider (or any other C# IDE), simply due to preference. I just feel like WinForms work flow is way better in Visual Studio.
Setup
After DoubleAgent is installed and configured, I found Bonzi's .acs file online (this file contains all graphics for the desktop companion". I will save a lot of the configuration and DoubleAgent usage for the previously mentioned tutorial, but just wanted to show the initial setup from the initial Form.
private const string AcsPath = "C:\\agents\\Bonzi.acs";
private const string AgentName = "Bonzi";
private const string TtsId = "{CA141FD0-AC7F-11D1-97A3-006008273000}";
These strings contain essentially all the data needed to get Bonzi up on the screen. AcsPath
is the path to the .acs file I found, AgentName
is the name of the agent being used and has to be set to its actual name. Lastly, TtsId
is the value of the voice being used for Text-to-Speech (Bonzi's voice). These are predetermined, and can be discovered using a tool like MASH.
_agent = new AxControl();
_agent.CreateControl();
_agent.Characters.Load(AgentName, AcsPath);
_agent.Characters[AgentName].TTSModeID = TtsId;
_agent.Characters[AgentName].MoveTo(Convert.ToInt16(Screen.PrimaryScreen.Bounds.Right - 600),
Convert.ToInt16(Screen.PrimaryScreen.Bounds.Bottom - 450), 500);
_agent.Characters[AgentName].Show();
Bonzi's mechanism is primarily programmed using the AxControl()
class. You can then see I use AgentName
and AcsPath
to tell DoubleAgent what to add its Characters
dictionary.
The last few lines simply position where Bonzi will appear (DoubleAgent only works with shorts numerically it seems). The very last line is what gets Bonzi on the screen granted all the provided variables are correct.
Logic
The way I set Bonzi up is as follows:
Bonzi has a catalog of phrases he can say.
The phrases said by Bonzi is dependant on a "SpeechType" property which is changed based on what event is fired.
After the property is set, the program uses a "Speech" interface to dictate which phrases from the catalog are said.
Depending on the event, a new Form might appear to prompt the user for information needed in a query string.
The program then sends an HTTP request to various free APIs with the constructed query string.
If successful, the result will either be recited by Bonzi or shown on a display form. Otherwise, Bonzi informs the user of some kind of error.
The program periodically checks if the user has network connection and handles events accordingly.
A simple
.txt
file is used internally for simple tasks such as reciting the user's name, the initial date the program was initialized so Bonzi can make funny remarks, etc.
That's pretty much the gist of it.
Example
Here I will break down and show the logic for Bonzi telling a joke.
Firstly, the user right-click's on Bonzi and click 'Joke'. Behind the scenes this triggers a ControlCommand
event that fires a button click event. ControlCommand
being proprietary to DoubleAgent, essential is Bonzi's context menu. It fires a lengthy switch statement where whatever option is selected fires a corresponding button click event from an older version I had where Bonzi was controlled using a Form or Control Panel of sorts.
Here is the code for Joke button event:
private void jokeButton_Click(object sender, EventArgs e)
{
_helper.Stop();
_bonzi.SetSpeechPattern(SpeechType.Joke);
_helper.Speak(_bonzi.Speak()!.GetPhraseDictionary()!["First"]);
RandomNumberHelper.SetIndex(5);
switch (RandomNumberHelper.CurrentValue)
{
case 0:
_helper.Play("Explain");
break;
case 1:
_helper.Play("Explain2");
break;
case 2:
_helper.Play("Explain3");
break;
case 3:
_helper.Play("Explain4");
break;
case 4:
_helper.Play("Surprised");
break;
}
_helper.Speak(_bonzi.Speak()!.GetPhrase());
RandomNumberHelper.SetIndex(2);
switch (RandomNumberHelper.CurrentValue)
{
case 0:
_helper.Play("Giggle");
break;
case 1:
_helper.Play("PleasedSoft");
break;
}
}
_helper
is a class I created that just handles and simplifies interacting with AxControl
. Otherwise, I would have to type _agent.Characters[AgentName].Stop()
or whatever every time I need to do something with the control. In this case, _helper.Stop()
stops Bonzi from whatever he's saying or animating, and frees his queue up for new events.
_bonzi
is of type Bonzi
, and is simply a model used for Bonzi's interactions while the program is running. In this case, I called SetSpeechPattern(SpeechType.Joke);
. Internally this method will call the proper API, and pick all the phrases necessary for that event before Bonzi does anything on the screen.
_helper.Speak(_bonzi.Speak()!.GetPhraseDictionary()!["First"]);
is called next. The .Speak()
is Bonzi's current Speech object, which either has a Dictionary<string, string>
called PhraseDictionary, or a List<string>
called PhraseList.
Note that before this point, the data is already retrieved from the HTTP Get request. If data wasn't retrieved successfully, none of the rest of the code would execute as it is handled in some try/catch somewhere.
The rest of this code simply just uses a RNG to play random animations from an array of animations within Bonzi's .acs
file. These animations can be printed to the debugger, or tested out in MASH.
The following is the code that gets run in SetSpeechPattern(SpeechType.Joke)
. Please note that SpeechType.Joke is simply an enum with the different types of speech results Bonzi has.
case SpeechType.Joke:
_speechPattern = new Joke();
break;
Pretty simple. _speechPattern
is an interface that at runtime can form of any child class that inherits from it, with its most abstract predecessor being an object called Speech
.
Here is the code for the Joke
class.
using BonzoBuddo.Helpers;
namespace BonzoBuddo.BonziAI.Speech;
public class Joke : Speech
{
/// <summary>
/// Default constructor for Joke.
/// </summary>
public Joke()
{
PhraseDictionary = Phrases.JokeExtras();
}
/// <summary>
/// Gets a random jok, using ApiHelper class and Ninja APIs
/// </summary>
/// <returns>Returns random joke using ApiHelper</returns>
public override string GetPhrase()
{
return ApiHelper.GetJoke();
}
/// <summary>
/// Method unsupported by implementation.
/// </summary>
/// <param name="index">Not used.</param>
/// <returns>See exception</returns>
/// <exception cref="NotSupportedException">Throws if this method is used with this child class.</exception>
public override string GetPhrase(int index) => throw new NotSupportedException("Use parameter-less GetPhrase() or GetPhrase(string key)");
The API here gets called in the abstract method public override string GetPhrase()
. The rest of the class either handles programming errors, ensuring the right abstract method gets used, and also sets the object's PhraseDictionary to a specific Dictionary in a static class that is only comprised of static Lists and Dictionaries.
Let's take a look at return ApiHelper.GetJoke()
. Which is in a static class I made that is solely responsible for HTTP Get Requests.
public static string GetJoke()
{
var client = new HttpClient();
try
{
var response =
client.GetStringAsync(
"https://somequerystring.net");
var data = JsonNode.Parse(response.Result)!;
var joke = data.Root["joke"];
Debug.Assert(joke != null, nameof(joke) + " != null");
return joke.ToString();
}
catch (JsonException e)
{
Debug.WriteLine(e.Message);
return "I cannot connect to the internet.";
}
finally
{
client.Dispose();
}
}
The above is the API call, which will probably be refactored at a later point so it can be using a using
directive. It returns the joke as a string and that is used in Bonzi's current PhraseDictionary. Nice!
In conclusion, Bonzi Buddy works as such:
User clicks on an item in context menu.
Item selected fires an event.
Bonzi stops what he is doing, and changed Speech Type
Changing the Speech Type instantiates a new implementation of Speech interface at runtime.
The constructor of implementation calls static class
ApiHelper.
ApiHelper
makes the HTTP request, parses the data, then returns the data (usually as a string or collection of strings) to the constructor, and sets the phrases to be said.Event uses
AxControl
to get Bonzi to speak whatever phrase was retrieved and other related phrases from the HTTP request, and acts accordingly.
For a deeper dive you can always check out the public repo for this project, which will eventually include the official release, but I will update when that time comes!
I hope you enjoyed this article! Please never hesitate to let me know of features you'd like to see, or ideas you have for this project either on Hashnode or via GitHub. In the repo, you can check out ROADMAP.md
for what I have planned for this project as well as proposed release dates. Feel free to fork and play around with it, if you would like to contribute, again just message me and I can add you as a collaborator!
/
Keep coding,
Michaël
Subscribe to my newsletter
Read articles from Michaël Landry directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by