Crates.io | me2finale |
lib.rs | me2finale |
version | 0.2.0 |
source | src |
created_at | 2023-02-06 03:40:20.183722 |
updated_at | 2023-02-07 03:00:17.161403 |
description | Mass Effect 2 final mission analysis |
homepage | |
repository | https://github.com/80Ltrumpet/me2finale |
max_upload_size | |
id | 777616 |
size | 96,047 |
This document assumes the reader is familiar with the entirety of Mass Effect 2, including references and terminology that may be considered spoilers. You have been warned! :)
This Rust crate defines types that encode decision paths and outcomes related to the final mission of Mass Effect 2. Although the game presents many, many choices to the player, only certain ones affect the survival of your allies into and through Mass Effect 3, if you transfer your save file. This crate is only concerned with those aspects of the game.
The data generated by the generate::outcome_map()
function is a map of
outcomes to metadata describing the decision paths that lead to those
outcomes. The metadata contains one example of a decision path and a count of
the total number of decision paths that lead to the same outcome, rather than
storing all of the decision paths—there are over 64 billion of them!
An Outcome
encodes the following information:
A DecisionPath
encodes the choices that affect the outcome. The examples
included in each outcome's metadata answer the following questions, some of
which are optional, depending on previous choices:
Some player actions for which consequences for allies are realized in Mass Effect 2 and beyond are not considered in the scope of this project. See Limitations for a discussion of the rationale behind the exclusions.
To generate the data yourself, run the generate
example provided in the
source.
$ cargo run --release --features generate --example generate -- PATH
Alternatively, you can reuse the data included in
outcome_map.rmp
. It is an OutcomeMap
serialized in the
MessagePack format (via the rmp-serde
crate).
To show the kind of questions that can be answered with the data, the following
statistics were generated with the provided analyze
example (and
re-formatted for markdown).
$ cargo run --example analyze -- outcome_map.rmp
The following table, which shows the absolute survival rates for each ally
under different conditions, is also generated by the analyze
example. Allies
are sorted in descending order by their total absolute survival rate.
Ally | Loyal | Disloyal | Total |
---|---|---|---|
Miranda | 0.51996 | 0.19516 | 0.71513 |
Jacob | 0.43366 | 0.15589 | 0.58954 |
Garrus | 0.37451 | 0.15837 | 0.53288 |
Zaeed | 0.32652 | 0.19610 | 0.52263 |
Grunt | 0.30619 | 0.18244 | 0.48864 |
Legion | 0.26426 | 0.10292 | 0.36718 |
Thane | 0.22933 | 0.13319 | 0.36252 |
Samara | 0.21059 | 0.14190 | 0.35250 |
Mordin | 0.30812 | 0.03100 | 0.33912 |
Jack | 0.24870 | 0.06639 | 0.31509 |
Kasumi | 0.26672 | 0.02895 | 0.29567 |
Tali | 0.26154 | 0.02397 | 0.28551 |
Morinth | 0.22909 | 0.00000 | 0.22909 |
NOTE: Survival implies recruitment. That is, if an ally is not recruited, they are not regarded as surviving.
The following subsections discuss some of the known and perceived limitations of this crate.
As previously mentioned, only as many decision paths are recorded in the data as there are outcomes even though there is a one-to-many relationship between them.
As a quick thought experiment, suppose each decision path could be somehow perfectly encoded and compressed into four-and-a-half bytes (36 bits). The decision paths alone would require almost 290 GB of memory/storage—and that's the best case!
In reality, the encoding would require more than 36 bits, so it's not reasonable to expect potential consumers of this crate to sacrifice so much of their storage to host the data. By embracing this limitation, it is possible to provide the fully-generated data within the crate's source repository so users don't have to spend the 10–15 minutes it would take to generate it themselves.
The scope of this project is limited to only the parameters that affect the fate of Mass Effect 2 allies when carried over to Mass Effect 3. If an ally survives ME2, they will be encounterable in ME3. Furthermore, if they are loyal, they may become a war asset in ME3. If they were not loyal, however, they would die in ME3.
However, there are two members of the crew of The Normandy SR2 who are not specifically addressed in this implementation: Dr. Karen Chakwas and YN Kelly Chambers. Outcomes only encode whether the crew was rescued, but that is only part of the story. Some of the crew may die before they are rescued based on the number of missions completed after the installation of the Reaper IFF.
Missions | Result |
---|---|
0 | Everyone in the crew survives. |
1–3 | Half of the crew dies, including YN Kelly Chambers. |
>3 | Everyone except Dr. Karen Chakwas dies. |
Both Chambers and Chakwas return in ME3 if they survive the final mission of ME2, and Chambers even has an "implicit loyalty" in ME2 that factors into her fate in ME3. So, why are they not explicitly considered?
In Dr. Chakwas's case, her survival is entirely dependent on whether an escort is selected, so rescuing the crew means, at the very least, that she survives.
However, the decisions leading to YN Chambers's loyalty and survival have no bearing on any of the choices the player can make during the finale of ME2. Any decision path can be arbitrarily annotated with all possible combinations of Kelly's loyalty and survival without changing anything else about the choices made in that decision path, and you would always end up with a valid decision path.
In the end, although the previously mentioned choices have consequences in ME3, the author considers them orthogonal to the decisions addressed in this crate.
This project was made possible by some amazing people in the Mass Effect community, particularly those who took the time to answer some esoteric questions on the subreddit. Of particular importance was this flowchart for which I regrettably am unable to find any attribution, though I believe that particular version was distilled from a primary source that is also unknown to me.
This crate is based on a nearly identical project I originally wrote in Python. The statistics included in that project's README are noticeably different than those above. I am not entirely sure why, though I suspect a subtle bug in the logic that allows the outcome data generation to be paused/continued. That mechanism was necessary to prevent unrelated problems (e.g., power outages) from causing days of computing time to be lost.
You might be asking, "Days? Really?" Yes, really.
In retrospect, I made some poor decisions in the Python implementation that I avoided this time around:
int
s, which have a variable bit width.
The last point turned out not to be as much of an issue in Rust. A naïve port of the Python implementation—minus the bit-packing and overly-eager disk I/O—took less than 90 minutes to generate all of the outcome data on my system. Not bad!
Things got really fast when I finally got around to using more of the CPU's
cores. With a combination of the rayon
and dashmap
crates, it was very
easy to parallelize the algorithm. However, applying rayon
to everything
eventually reaches a point of diminishing returns, so I only changed enough
to maximize the speed-up. It turns out that parallelizing the first three
levels of recursion was sufficient. As a result, it now takes just over ten
minutes to generate all of the data. Good enough!