Holiday Hack 2023: Squarewheel Yard
Geography
Getting To
Before I head over to the space Island, I’ll notice three Gameboy objectives in my badge and go exploring. At the backside of the Island of Misfit Toys, I’ll find Squarewheel Yard:
Location Layout
The port has a very Misfit Toys vibe:
The Goose greets me with:
Good of the Island of Misfit Toys
Beep beep
Luggage Lock Decode
Challenge
This area adds the Luggage Lock objective to the badge:
Garland Candlesticks is in the center of the area next to a suitcase:
Garland Candlesticks
Hey there, I’m Garland Candlesticks! I could really use your help with something.
You see, I have this important pamphlet in my luggage, but I just can’t remember the combination to open it!
Chris Elgee gave a talk recently that might help me with this problem. Did you attend that?
I seem to recall Chris mentioning a technique to figure out the combinations…
I have faith in you! We’ll get that luggage open in no time.
This pamphlet is crucial for me, so I can’t thank you enough for your assistance.
Once we retrieve it, I promise to treat you to a frosty snack on me!
Game
The game offers a chance to select a difficulty:
I get luggage with the number of wheels matching what I select:
The help button shows up to interact:
It’s worth noting that clicking on the button (or pushing space) presses in that button a little bit each time, and after 5 or 6 presses, it pops back out. This is meant to represent the amount of pressure being put on the button.
Solve
Chris Elgee’s talk at KringleCon 2023, Lock Talk, talks about how to attack this kind of lock. Basically, I want to put some pressure on the button and look for where there’s resistance to turning the dials. Where it gets stuck, that’s a good sign that it’s the right one, though any position can get resistance with too much pressure and/or bad luck.
On entering the correct code, the bag opens:
Hack
This challenge is a fun one to hack as well. I’ll look at the websocket messages and show two ways to cheat the challenge:
- Looking at the setup message to figure out the combo.
- Sending 10,000 “Open” messages with each combination possibility from the dev tools console.
Full details in this video:
Epilogue
Garland Candlesticks
Wow, you did it! I knew you could crack the code. Thank you so much!
Bonus! Fishing
Challenge Part 1
Poinsettia McMittens (who must be a twin of Garland) is standing by the dock with some information about fishing:
Poinsettia McMittens
Excuse me, but you’re interrupting my fishing serenity. Oh, you’d like to know how to become as good at fishing as I am?
Well, first of all, thank you for noticing my flair for fishing. It’s not just about looking good beside the lake, you know.
The key is in the details, much like crafting the perfect toy. Observe the water, the weather, and the fish’s habits - it’s a science and an art.
Of course, it helps to have a natural charm. Fish seem to find me irresistible. Must be my sparkling personality… or maybe it’s just the glitter of my allure.
Oh, the mysteries of the aquatic life around these islands are as elusive as, well, a clever compliment. But you’ll get one if you probe enough.
Remember, patience is more than a virtue in fishing; it’s a strategy. Like waiting for the right time to use flattery, you wait for the right moment to strike.
Go see if you can catch, say, 20 different types of fish!
Fishing Legit
Fishing is easy enough. I make sure my boat is at a stop in the water, and click the “Cast Line” button:
The button turns to “Reel it in”:
After some amount of time, it will turn red:
Clicking then (and before it goes back to white) will catch a fish, which opens the Pescadex and shows details about it. For example:
With some patience and a few minutes, I’ll catch 20 fish and head back to Poinsettia.
Challenge Part 2
Poinsettia ups the stakes:
Poinsettia McMittens
Hoy small fry, nice work!
Now, just imagine if we had an automatic fish catcher? It would be as ingenious as me on a good day!
I came across this fascinating article about such a device in a magazine during one of my more glamorous fishing sessions.
If only I could get my hands on it, I’d be the undisputed queen of catching them all!
On talking, two objectives show up (one already solved):
Find Heat Maps
At the top of the body of the main sea
page, there’s a commented out HTML link:
This leads to https://2023.holidayhackchallenge.com/sea/fishdensityref.html
, which is a page that has black and white squares for each of a bunch of fish:
Source Analysis
The goal here is to figure out what triggers a fish on the line. At line 327 of client.js
, there’s an event listener created for incoming message:
It does a long if
/ else if
block for the different starting letters of the message. At first I was very intriged by f:
:
} else if (messageType === 'f:') {
const parsed = JSON.parse(payload);
const idx = playerData.fishCaught.findIndex(fish => fish.name === parsed.fish.name);
if (idx === -1) {
playerData.fishCaught.push(parsed.fish);
pescadexIndex = playerData.fishCaught.length - 1;
freshCatch = true;
} else {
pescadexIndex = idx;
}
cotd.classList.add('visible');
updatePescadex();
}
It turns out this message comes from the server after I push “Reel it in”. So that’s not much use. At the end after all the messages are processed, it calls UpdateUI()
regardless which message was called. In that function, this section jumps out:
if (me.fishing) {
reelItInBtn.style.display = 'block';
if (me.onTheLine) {
reelItInBtn.classList.add('gotone');
reelItInBtn.innerText = 'Reel it in!';
} else {
reelItInBtn.classList.remove('gotone');
reelItInBtn.innerText = 'Reel it in';
}
} else {
reelItInBtn.style.display = 'none';
}
me
is defined earlier as Entities[playerData.uid]
. Outside that function, I don’t have access to me
. But Entities
and playerData
do exist:
If I grab the Entities
entry for my uid
, it has fishing
, fishCaught
, and oneTheLine
:
Listening on Socket
POC
I want to try to listen on the socket, so I’ll mimic what’s in the code in my console:
Right away, there are a ton of v:
messages. I’ll re-run to filter those out (refreshing the page to reset the console). At first there’s nothing, but as soon as I start moving, there’s a bunch of e:
messages:
Finding e Messages
I’ll refresh again and add some information about the payload. When I move, e:
messages provide the update as to where my boat is and where it’s going:
But when I fish, must larger e:
messages come through:
These are the updates that set onTheLine
!
Sending on Socket
I want to be able to automatically do things, so I need to know if there’s a message I can send that will “Reel it in”. At line 1007, there’s an event listener defined for when that button is clicked:
const reelItInBtn = document.querySelector('button.reelitin');
reelItInBtn.addEventListener('click', () => {
socket.send(`reel`);
window.top.postMessage({
type: 'sfx',
filename: 'fishing-reel.mp3',
}, '*');
});
It looks like all I have to do is send “reel” over the websocket. I’ll test it, by entering socket.send('reel');
into the console while I’m fishing, and observer that the UI updates to show I’m no longer fishing. I can wait until there’s a fish on the line and then send, and it does catch the fish!
Auto-Fish
I’ll use the following code to automatically fish:
socket.addEventListener("message", event => {
const messageType = event.data.substr(0, 2);
const payload = event.data.substr(2);
if (messageType == "e:") {
const parsed = JSON.parse(payload);
if (playerData.uid in parsed) {
const data = parsed[playerData.uid];
if ('onTheLine' in data && data.onTheLine) {
const idx = playerData.fishCaught.findIndex(fish => fish.name === data.onTheLine);
if (idx !== -1) {
console.log("Already have " + data.onTheLine);
} else {
socket.send('reel');
console.log(data.onTheLine + ' [' + (playerData.fishCaught.length + 1) + ']');
setTimeout(() => {socket.send('cast');}, 500);
}
}
}
}
});
It listens on the websocket, and when there’s an e:
message, it checks for onTheLine
and if it’s true
. If so, it looks in my current caught fish to see if I already have it, and if not, it sends the reel
message over the socket. It then prints a message to the log, and casts again in half a second.
If I want to not fish, all I have to do is manually reel it in, and then it won’t recast until I cast again and it catches something.
Using Heatmaps
Manually
Some fish are tricky to find, and I will need to be in just the right place. I could manually overlay the map with the heatmaps, but that seems like a lot of work.
In the source, there’s a dict ImageAssets
that defines the parts of the map:
Interestingly, by the time they get to my current context, they are not strings, but HTML objects:
These are used each time the UI is updated. I’ll try replacing the minimap.png
with a heatmap:
The map in the bottom right updates:
Now I can move around finding hot spots (where it’s very light) and fish there. This is particularly useful for the Piscis Cyberneticus Skodo, which is only found on one distinct space on the map:
Automate
I don’t want to have to keep finding the links and updating them. I’ll use curl
with some bash
-foo to get a list of the fish:
$ curl https://2023.holidayhackchallenge.com/sea/fishdensityref.html -s | grep h3 | cut -d'>' -f2 | cut -d'<' -f1 > SansHolidayChallenge-2023/fish.txt
I’ll use vim
to quickly create a JavaScript array from this list, and then use that to make the following function:
function update_map() {
var fishlist = ['Flutterfin Rainbow-Roll', 'Gelatina Ringletfin', 'Funfetti Flick-Flick', 'Fluffle-Muffin Sparklefin', 'Jovian Jamboree Jellydonut Jellyfish', 'Gumball Glooperfish', 'Jester Jellyfin', 'The Chocolate Star Gingo Guppy', 'Whirly Snuffleback Trout', 'Jolly Jellyjam Fish', 'Lounging Liquorice Crustacean-Nosed Berryfin', 'Jinglefin Jellyfrizzle', 'The Polka-Dot-Propeller Puffling Fish', 'JubiliFLOPinear Snorkeldonut', 'Glittering Gummy Guppy', 'Whiskered Whizzler', 'Flamango-Buzzling Sushi Swimmer', 'Flutterfin Bubblegum Gumball', 'ChocoSeahorsefly', 'Speckled Toastfin Snorkelback', 'Jamboree Jellywing', 'Fantabulous Fry-Sherbert Aquapine', 'Jamboree Jellofish', 'Bubblegum Blowfish-Bee', 'Frizzle Fringe Flutterfin', 'Whiskered Jumblefish', 'Bumblefin Toffee Torpedo', 'Jolly Jellypeanut Fish', 'Gumbubble Guppy', 'Glittering Gummy Whipray', 'Flippity Flan Flopper', 'Biscuit Bugle-Tail Fish', 'Strudel Scuttle Scalefish', 'Bubblegum Bumblefin', 'Flutterfin Falafeluncher', 'The Splendiferous Spaghetti Starfin', 'The Rainbow Jelibelly Floatfish', 'Rhinoceros Beetle Bumble Tuna', 'Hatwearing Hippofish', 'Frizzle Fish', 'Polka-Pop CandyFloss Fish', 'Bumbleberry Floatfish', 'Caramelotus Humming Float', 'Dandy Candy Goby', 'The Polka-Dot Pudding Puff', 'Frosted Donut Jellyfluff Puffer', 'Flamingo Flapjack Finaticus', 'The Splendiferous Spaghetti Seahorsicle', 'Flutterfin Scoopscale', 'Frizzle Frazzle Fly-n-Fish', 'Frizzle-Frizzled Jambalaya Jellyfish', 'Sprinkfish', 'Fantail Flutterfin', 'JellyChip CuddleSwimmer', 'Whiskered Lollipop Loonfish', 'Jester Gumball Pufferfish', 'The Hummingbrewster BumbleFlish', 'Jangleroo Snackfin', 'Blibbering Blubberwing', 'The Bubblegum Confeetish', 'Fantastical Fusilloni Flounderfish', 'Pizzafin Flutterbub', 'The Whiskered Watermelon Pufferfish', 'The Bumblebee Doughnut Delphin', 'Pistachio Pizzafin Puffinfly', 'Aquatic JellyPuff Doughnut Shark', 'Gumball Guppygator', 'The Burgerwing Seahorse', 'Bellychuckle Balloonfish', 'FizzleWing PuffleGill', 'Bumbleberry Rainbow Flicorn Fish', 'Whistlefin Wafflegill', 'Pizzadillo Glitter-Guppy', 'Jamboree Jellydonut Jellyfish Trout', 'The Bubblegum Bumblefin', 'Gelatino Floatyfin', 'The Frambuzzle Flickerfin', 'The Speckled Pizzafin Fizzflyer', 'Sparkling Pizzafin Pixie-fish', 'Bumblecado Finstache Hybridsail', 'Pizzamarine Popcorn Puffer', 'Laughter Ligrolomia', 'Frosted Jelly Doughnut Pegasus Finfish', 'The Whirling Donut Jellygator', 'Flutterfin Cupcake Goby', 'The Gumball Guppy', 'Bubblegum Blowfish Beetle Bug', 'Sparkling Gumbubble Piscadot', 'The Flamboyant Flutter-fish', 'Twinkling Tortellini Trouterfly', 'Beatleberry Fluff Guppy.', 'Glaze Meringuelle', 'The Whiskered Blubberberry Flapper', 'Sherbet Swooshfin', 'Marzipoisson Popsicala', 'Bubblegum Ballistic Barracuda', 'Puzzletail Splashcake', 'Fantasia Fluffernutter Finfish', 'Rainbow Jelly-Dough Fish', 'Flutterfin Pizzapuffer', 'BugBrella Aquacake', 'Twirly Finny Cakeling', 'Frizzleberry Flapjack Fish', 'Whiskered Sprinkle Glider', 'The Pristimaela Parfait Pengu-Angel', 'Bubblerooni WhiskerWaffle', 'The Speckled Whisker-Spoon Puffer', 'BumbleSquid Donutella', 'Sparkleberry Gobblefin', 'Fizzgiggle Frizzlefin', 'JibberJelly Sundae Swimmer', 'The Flutterfin Pastry Puffer', 'Rainbow Gummy Scalefish', 'Jingle JellyFroth Fish', 'The Spotted Flutterfin Pastrytetra', 'Flutterfin Hotcheeto Penguinfish', 'Piscis Cyberneticus Skodo', 'Oreo OctoPufferRock', 'Fluffernutter Pufferpine', 'Whirlygig Polka-Dotted Jelly-Donut Pufferfish', 'Bumbleberry Gilled Glider', 'Polkadot Pancake Puffer', 'Mermacorn Fish', 'Sprinkle Starfish Sardine', 'Choco-Bumblefin Parrot Trout', 'The Fantabulous Gala Glazed-Guppy', 'Pudding Puff ParrotMoth Fish', 'Fantastical Flapjack Flipperfin', 'TruffleBugle ZephyrFish', 'Bumbleberry Glitterfin', 'The Jester Jellycarafe', 'The Flamingotuna McSprinklefin', 'Whiskerfroth Flutterfin', 'Spotted Sprinkledonut Puffer', 'Stripe-tailed Pepperoni Puffer', 'Jelly-Feather Macaroon Guppy', 'Flutterfin Pancake Puffer.', 'Whiskered Rainbow Glidleberry', 'Chucklefin Clownfish', 'Bumbleberry Snorkelsnout', 'Jolly Jellydozer', 'The Polka Dotted Jello-fish', 'The Bumbleberry Guppiesaurus', 'Flutterfin Pizzacrust Glimmertail', 'Bumblebee, Pizza-fin Jamboree', 'Whizzbizzle Poptuckle', 'Candyfloss Clownphino', 'Flutterglaze Bumblefin', 'Bumbleberry Poptarticus', 'Plaid Zephyr Cuddlefin', 'Jolly Jambalaya Jubilee Fish', 'Confetti Clownfrippery Fish', 'Rainbow Jelly-Bumble Shark', 'Marshmallow Pogo-Starfish', 'The Spangled Jelly-Tortle Ripplefin', 'Fantabulous Rainbow Polka Poptartfish', 'The ChocoChandelier Goldnipper', 'Gummybrella Anemofin', 'Gummy Fizzler', 'The Bumblebelly Polkadot Glaze-fish', 'Fantaray Flakefin', 'Splendiferous Ribbontail', 'The Butterfleagleberry Seahorse', 'Sushinano Sweetsquid', 'The Whiskered Melonfin', 'The Fantastical Fizzbopper', 'Splashtastic Bagelback Rainbownose', 'Pizzafly Rainbowgill', 'Frizzling Bubblehopper', 'Cuckoo Bubblegum Unicornfish', 'The Lucid Lollyscale'];
var i = 0;
var idx = 0
while (idx >= 0 && i < fishlist.length) {
idx = playerData.fishCaught.findIndex(fish => fish.name === fishlist[i]);
if (idx < 0) {
console.log("Looking for: " + fishlist[i] + " [" + i + "]");
ImageAssets['minimap'].src = 'https://2023.holidayhackchallenge.com/sea/assets/noise/' + fishlist[i].replace(/ /g, '%20') + '.png'
}
i = i + 1;
}
}
It will loop through the fish list until it finds one that isn’t in the list, and then update the map. I’ll run setInterval
in the console to run it every 30 seconds:
setInterval(update_map, 30000)
Epilogue
Poinsettia is impressed:
Poinsettia McMittens
You managed to catch every fish? You’re like the fishing version of a Christmas miracle!
Now, if only you could teach me your ways… but then again, I’m already pretty fabulous at everything I do.