Table of Contents
Open Table of Contents
Introduction 📖
I’ve been playing Final Fantasy XI lately, and for the first time I wanted to level up my crafting skills.
One of the first things I did was to look up the crafting material requirements for each crafting recipe. Unfortunately, the only way to do that was to look up the recipe in the game wiki. After a couple of days this was getting tiring, since many recipes require other items to be crafted first.
And that’s how this little project was born. It’s still a work in progress, but I’m happy with the results so far.
In this post, I’ll go over the process of creating this project, starting with the initial concept that sparked its development.
Concept 💡
The idea was to create a tool that would calculate the material requirements for a given recipe.
Early in this project, I chose Go as my programming language. Not only did I want to learn this new language, but I was excited to create something that would directly benefit my gaming experience.
Inspired by reading the excellent book Let’s Go by Alex Edwards, I initially considered creating a web application to implement some of the concepts covered in the book.
With this idea in mind and the book as my guide, I started taking my first steps into the project.
Data and Modeling 🗂️
I started by looking for data on the crafting recipes. Thankfully, I found a JSON file containing the crafting recipes that I needed to make this tool work.
After finding the data, the next step was to determine how to store and manage it effectively. Since I had been reading about databases in the book, I decided to explore using one for this project. After evaluating different options, I chose SQLite3 thanks to its simplicity and perfect fit for a small-scale application like this.
Then, with the help of ChatGPT, I converted the JSON file into a database. With the database ready to go, I started running some queries to get a better idea of what the data would look like.
1. Database Structure And Go Types
The database structure needed to be simple yet effective. I created two main tables:
- A recipe table called
recipes
storing the basic recipe information like name and crystal cost. - An ingredient table called
ingredients
with foreign keys to link ingredients to their respective recipes.
The data from the database needed to be represented in our Go code. To achieve this, I created two types: a Recipe
type for recipe information, and an Ingredient
type for tracking required crafting materials.
type Recipe struct {
ID int
Name string
Crystal string
Ingredients []string
}
type Ingredient struct {
ID int
RecipeID int
Name string
}
With the database structure and types defined, the next step was to establish a connection between our Go application and the SQLite database.
2. Go And Database Drivers
In Go, we need to use what is called a database driver to connect to a database. It’s a package that provides a way for our program to communicate with the database.
After looking for a bit, I found modernc.org/sqlite, which is a driver that doesn’t require Cgo (a tool that enables interoperability between Go and C code). I’ve read that Cgo is a bit of a pain to work with, and I wanted to avoid it.
After selecting a suitable driver, I proceeded to set up the database connection for our application.
3. Connecting To The Database
Every database driver has its own way of connecting to the database. In the case of modernc.org/sqlite
,
I used the sql.Open()
function to connect to the database.
db, err := sql.Open("sqlite", "recipes.db")
if err != nil {
...
}
We need to pass the name of the driver and a path to the database file, in this case sqlite
and recipes.db
.
With the database connection established and working, I needed to decide on how to present the information to users. Initially, I decided to start with printing an output to the console.
Looking back at it now, I think I should have started figuring out how to present the information in the go templates, make the server return the data in JSON format, create the proper routes, etc.
Pivot: Web app to CLI 🗔
I initially aimed for a web app after reading Alex Edwards’ Let’s Go, but backend concepts slowed me down. To keep momentum, I pivoted to a CLI, focusing on data modeling and the core algorithm first. I plan to revisit the web app once the foundations are solid.
1. Implementing recursive breakdown
The core challenge was expanding sub-recipes until I reached base ingredients, then aggregating totals. I used a recursive traversal: if an ingredient is itself a recipe, descend into it; otherwise, add it to the final tally. Indentation in the output mirrors depth, making the hierarchy easy to scan.
I created a function called printIngredientBreakdown
that I’m really proud of.
func printIngredientBreakdown(
db *sql.DB,
ingredientCount map[string]int,
finalIngredientList map[string]int,
finalCrystalList map[string]int,
quantity int,
depth int) {
indent := strings.Repeat("\t", depth)
for key, value := range ingredientCount {
isRecipe := isIngredientRecipe(db, key)
value = value * quantity
if !isRecipe && depth >= 1 {
continue
}
fmt.Printf("%dx %s | Is recipe: %v\n", value, key, isRecipe)
recipe := getRecipe(db, key)
recipe = getIngredients(db, recipe)
ingredientSubCount := countIngredients(recipe)
for subKey, subValue := range ingredientSubCount {
isSubIngredientRecipe := isIngredientRecipe(db, subKey)
fmt.Printf("%s\t%vx %s\n", indent, value, recipe.Crystal)
fmt.Printf("%s\t%dx %s | Is recipe: %v\n", indent, subValue*value, subKey, isSubIngredientRecipe)
finalCrystalList[recipe.Crystal] = finalCrystalList[recipe.Crystal] + value
if isSubIngredientRecipe {
fmt.Printf("=== Sub Recipe: %s ===\n", subKey)
}
if !isSubIngredientRecipe {
finalIngredientList[subKey] = finalIngredientList[subKey] + subValue*value
}
}
printIngredientBreakdown(db, ingredientSubCount, finalIngredientList, finalCrystalList, value, depth+1)
}
}
This function takes in a map[string]int
of the ingredients and a quantity of the recipe you want to make, then it
recursively prints a detailed breakdown of ingredients required for a recipe.
It shows the quantity needed for each ingredient and indicates whether each ingredient is itself a recipe.
For recipe ingredients, it will further break down their sub-ingredients with proper indentation.
Nailing down the recursion was the hardest part for me. I had to think about how to handle the case where an ingredient is a recipe, and how to handle the case where an ingredient is a sub-ingredient of a recipe. But after a lot of headbanging and some reading, I finally got it right.
2. User inputs and the moment I almost gave up
Celebrating the success of the recursion, I decided to add a few more features to the CLI app. I wanted to allow users to input the quantity of the recipe they want to make, and also to allow users to input the name of the recipe they want to make.
This implementation at first seemed straightforward, and for a while I thought I was done. But then I realized that the user input was not being handled properly. The interesting thing was that the input was being handled properly in the IDE but not in the compiled binary.
For the input handling I was using the package bufio
, which provides a way to read input from the console, differently from the
fmt
package and its Scan
functions. At first, I was using bufio.Reader
, it worked like a charm inside the IDE,
but for some reason the input in the compiled binary was utterly broken.
Panic sets in, I thought I was getting closer to finishing the project, but this setback hit harder than I expected. I didn’t understand why it was broken, and I was stuck.
Testing different things, I found that the input was passing in the \n
character, which was causing the input to be broken,
despite having measures in place to handle this, the input was still broken.
Seeing the light at the end of the tunnel, I decided to use bufio.Scanner
which handles whitespace and newline-delimited input more reliably.
The Victory 🎉
After a few more hours of debugging, I was able to get the CLI app working properly.
Here’s a simple recipe and a nested one. Note how sub-recipes expand inline and the final section aggregates base materials and crystals.
Simple recipe:
FFXI Crafting Calculator
=========================
Type 'exit' to quit.
=========================
Enter Item Name: grass thread
Enter Item Quantity: 1
=== Ingredients List for 'Grass Thread' ===
1x Lightng. Crystal
2x Moko Grass | Is recipe: false
==== END OF LIST ====
Enter Item Name:
Nested recipe:
=== Ingredients List for 'Slops' ===
1x Earth Crystal
1x Cotton Thread | Is recipe: true
1x Lightng. Crystal
2x Saruta Cotton | Is recipe: false
1x Grass Cloth | Is recipe: true
1x Earth Crystal
3x Grass Thread | Is recipe: true
=== Sub Recipe: Grass Thread ===
3x Grass Thread | Is recipe: true
3x Lightng. Crystal
6x Moko Grass | Is recipe: false
2x Cotton Cloth | Is recipe: true
2x Earth Crystal
6x Cotton Thread | Is recipe: true
=== Sub Recipe: Cotton Thread ===
6x Cotton Thread | Is recipe: true
6x Lightng. Crystal
12x Saruta Cotton | Is recipe: false
=== Final Ingredients List ===
14x Saruta Cotton
6x Moko Grass
4x Earth Crystal
10x Lightng. Crystal
==== END OF LIST ====
Success at last!
Closing Thoughts 💭
Despite the challenges I faced, I’m thrilled with the result of this project. I learned a lot about Go, and I’m looking forward to continuing to work on it.
This project was a great learning experience for me, from working with a database to creating a CLI app.
It also taught me about user inputs, fmt
and bufio
, and how to handle them properly.`
Database drivers and how to use them to connect to a database.
How to do a recursive function.
And most importantly, perseverance against challenges.
Creating a tool to help me with a game I love to play is something I’ve always wanted to do, and I’m glad I was able to do it.
I’m looking forward to continuing to work on this project and creating more features for it.
Things I’m considering doing in the future:
- Better-aligned, colorized output for readability.
- TUI (e.g., for search, quantity tweaks, and live totals).
- Web app with JSON API backend and a React UI.
If you reached this point, I hope you enjoyed reading this post.
See you next time!