This code was originally made for Vanellope's unlock quest, but strictly speaking, it is not really a "Vanellope-only" code. The actual purpose is more specific:
It forces the two DreamSnaps-related mission objectives to advance even while offline.
The two problematic steps are:
- Opening the DreamSnaps menu/page to view the current DreamSnaps information.
- Submitting something to DreamSnaps.
Both of these normally require an online connection. If the game is offline, those steps cannot be completed normally. So the point of the code is not to skip Vanellope's whole mission. It is only meant to handle those two online-only DreamSnaps blockers.
If another mission used the same kind of DreamSnaps-related objectives, the same idea could apply there too.
Why I used the tracked mission function
One of the first problems was: how do I tell the code which mission I want to process?
The game can have multiple active missions at the same time, but the player can only track one mission at once. That made the tracking system a convenient entry point. When the tracked activity changes, the game receives information about the newly tracked activity. So I searched dump.cs for a function related to that and found:
Code:
Mdl.Missions.MissionManager$$OnTrackedMissionChanged
In IDA, this function was very small. It basically just branched to:
Code:
Mdl.Missions.MissionManager$$RefreshCurrentFollower
That made it a good hook point:
- It runs when the player selects a tracked mission.
- The tracked activity is passed as an argument.
- If the tracked activity is a mission, the missionId can be read from that argument.
- The original function is small, so it is easy to preserve the original behavior.
- It was also convenient for a code cave hook.
The missionId itself is not the real filter. It is just a convenient way to get the MissionSlot for the mission the player is currently tracking.
Getting the missionId
The tracked activity object can represent different kinds of tracked activities, not just missions. So the code first checks whether the tracked activity is actually a mission:
Code:
TrackedActivity.activityCase_ == Mission
If it is not a mission, the code just returns to the original behavior. If it is a mission, the code reads the TrackedMission object and gets the missionId from it:
Code:
if trackedActivity.activityCase_ != Mission:
return RefreshCurrentFollower()
missionId = trackedActivity.activity_.missionId
Again, the missionId is only used to find the currently tracked MissionSlot. The code does not depend on a hardcoded Vanellope missionId.
Keeping the original behavior
After getting the missionId, the code still calls the function that the original code was going to call:
Code:
MissionManager.RefreshCurrentFollower()
The hook is not meant to replace the original behavior. It only adds extra logic after the normal tracked-mission update has happened.
Getting the current mission data
Once the code has the missionId, it gets the actual mission data from the profile. The path is roughly:
Code:
MissionManager
-> Profile
-> ProfileWorld
-> GetMissionSlot(missionId, null)
So the code gets the ProfileWorld and calls:
Code:
ProfileWorld.GetMissionSlot(missionId, null)
That returns the MissionSlot for the currently tracked mission.
Walking from the mission to the actual objective
The mission structure is nested. It is roughly:
Code:
MissionSlot
-> CurrentStep
-> SubSteps
-> Objectives
-> ObjectiveData
So the code gets the current step:
Code:
MissionSlot.get_CurrentStep()
Then it loops through the SubSteps, and inside each SubStep it loops through the Objectives. This is why the code has a double loop. The mission itself is not enough. I needed to reach the actual MissionObjective inside the current step.
Filtering the objectives
For each MissionObjective, the code first checks the status. It only handles:
Code:
Ongoing = 1
ReadyToBeCompleted = 2
Anything else is skipped. Then it checks:
Code:
MissionObjective.Data.customStepCase_
For the DreamSnaps-related blockers, the relevant objective types are:
Code:
MenuAction = 207
DesignChallenge = 140
In this quest, the MenuAction objective is the DreamSnaps menu/page step, and the DesignChallenge objective is the DreamSnaps submission-related step. So the logic is basically:
Code:
if customStepCase_ == MenuAction:
condition = MenuAction
else if customStepCase_ == DesignChallenge:
condition = ForceSkipDesignChallenge
else:
skip it
The condition values passed to the advance function are:
Code:
MenuAction = 13
ForceSkipDesignChallenge = 14
This keeps the code focused on the two objective types that were blocking offline progress, instead of force-advancing unrelated mission steps that could have much bigger side effects.
Why I did not skip the whole mission
I did not want to force-complete the whole mission. That would be risky because some quest steps do more than just update the objective text. They can give items, unlock objects, initialize later quest data, or change the world state.
For example, Vanellope's quest includes a step where the player has to place Vanellope's house. If the mission was skipped too far, that step might never initialize correctly. In the worst case, the player could end up unable to place Vanellope's house properly.
That is why I chose to advance only the specific offline-blocking objectives instead of skipping the mission itself.
Why I used CheckForAdvanceStep
This was the part that took the most trial and error. There was also a more direct way to force the mission forward, but it did not trigger all of the events or notifications correctly.
For example, the objective could appear to advance, but later quest logic did not initialize properly. One bad result was that Vanellope's house was not added to the inventory when the quest reached the part where it should be placed. So directly forcing progress was not enough.
Mission progress is not just:
There are other things tied to it:
- objective completion handling
- step advancement
- event dispatch
- notifications
- UI updates
- item or reward handling
- next-step initialization
The function I ended up using was:
Code:
Mdl.Missions.MissionManager$$CheckForAdvanceStep
This function takes the condition, the MissionSlot, and the stepName:
Code:
MissionManager.CheckForAdvanceStep(
condition,
missionSlot,
stepName
)
So instead of simply editing the objective status, the code sends the objective through a function that checks and advances the mission step more properly. That made the forced progress work with the required event/notification behavior instead of only changing the visible state.
The workflow
In practice, the workflow was:
- Choose a function where the target mission can be identified.
- Use the tracked mission argument to get the missionId.
- Use that missionId to get the MissionSlot.
- From the MissionSlot, get the current step.
- Use a double loop to reach the actual MissionObjective.
- Filter only the DreamSnaps-related objective types.
- Pass the matching condition to CheckForAdvanceStep.
So the code is not really a "complete Vanellope quest" code. It is closer to a targeted workaround for the two DreamSnaps objectives that normally cannot be completed offline.