Files: Startup_*.pcc, SFXGame.pcc
Needs fixed in localized file? Yes
Related issues:
Files modified/added for LE1 Community Patch:
LE1SubtitleFix-LE1CP.zip
Installation with M3 via LE1 Community Patch verified.
Background
In the original Mass Effect 1, GUI_SF_Conversation.Conversation was responsible for both the conversation wheel and subtitle rendering.
In later games, BioWare split subtitle rendering into a separate native-only Render Thread UI (FSFXGFx_RTGuiManager) while leaving the conversation wheel in the standard GFx UI.
For Legendary Edition, this Render Thread system was backported into LE1. Rather than introducing a dedicated subtitle UI, BioWare repurposed GUI_SF_LoadScreenToolTips.LoadScreenToolTips.
As a result, this UI now performs two unrelated functions:
- Displays the "Loading" text during loading screens.
- Displays subtitles during FMVs.
Because normal GFx UIs such as GUI_SF_Conversation.Conversation are not rendered over FMVs, all FMV subtitles are routed through this Render Thread UI instead.
This implementation introduces the issues described in #306.
In addition, the Render Thread UI only receives:
- an HTML-formatted subtitle string (with an undefined color and a hardcoded font size of 22), and
- a Boolean indicating whether the text is a subtitle or loading text.
Consequently, FMV subtitles ignore the user's configured subtitle size (including launcher settings and custom launch parameters such as those provided by ME3Tweaks Mod Manager), causing issue #133.
For executable reverse engineering, search for the string "setLoadingText" in Ghidra and inspect its references. The function that sends subtitle text to LoadScreenToolTips invokes the standard GFx InvokeMethod() API.
Proposed Solution
Instead of rendering FMV subtitles through GUI_SF_LoadScreenToolTips, redirect them back to GUI_SF_Conversation.Conversation, restoring a workflow much closer to the original Mass Effect 1 implementation.
Because the Render Thread interface only exposes two parameters (text and subtitle/loading state), subtitle formatting data must be embedded into the transmitted string and reconstructed by the receiving Scaleform movie.
The implementation consists of:
- Redirecting the Render Thread subtitle resource to
GUI_SF_Conversation.Conversation.
- Adding a
setLoadingText() entry point compatible with the Render Thread interface.
- Encoding subtitle formatting parameters into a hidden
__PARAMS[...] prefix.
- Extending
setSubtitle() to extract those parameters before rendering.
- Making the required Scaleform asset and layout adjustments.
- Updating
BioSeqAct_FaceOnlyVO to prepend subtitle formatting information before dispatching subtitles.
- Adding a new "silent"
FaceOnlyVO object to the Citadel arrival cinematic so its subtitles enter the Render Thread subtitle pipeline.
This is the closest solution to restoring the behaviour seen in the original Mass Effect 1 without resorting to ASI modding.
Implementation
1. Redirect the Render Thread subtitle resource
Modify FSFXGFx_RTGuiManager in BIOUI.ini
[BIOUI.ini FSFXGFx_RTGuiManager]
-FSFXGFx_RTLevelLoadTip_Resource=GUI_SF_LoadScreenToolTips.LoadScreenToolTips
+FSFXGFx_RTLevelLoadTip_Resource=GUI_SF_Conversation.Conversation
2. Add setLoadingText() to Startup_*.GUI_SF_Conversation.Conversation
function setLoadingText(txt, bIsSubtitle)
{
HideWheel();
// --- PRE-PROCESSOR: HTML TAG SCRUBBER ---
// Loop to strip out closing </font> tags
while (txt.indexOf("</font>") != -1) {
var closeStart = txt.indexOf("</font>");
txt = txt.substring(0, closeStart) + txt.substring(closeStart + 7);
}
// Loop to strip out dynamic opening <font ...> tags containing attributes
while (txt.indexOf("<font") != -1) {
var openStart = txt.indexOf("<font");
var openEnd = txt.indexOf(">", openStart);
if (openEnd != -1) {
// Cut out everything from "<font" up to and including the closing ">"
txt = txt.substring(0, openStart) + txt.substring(openEnd + 1);
} else {
// Fail-safe protection against malformed HTML tags to prevent infinite loops
break;
}
}
// ----------------------------------------
// Apply structural fallback properties
var finalSize = bIsSubtitle ? 22 : 16;
var finalColor = bIsSubtitle ? 13434879 : 10079436;
var finalAlpha = 255;
var finalPosition = bIsSubtitle ? 1 : 5;
var cleanTxt = txt;
// Locate the start of the identifier tag
var prefixStart = txt.indexOf("__PARAMS[");
if (prefixStart != -1)
{
// The actual values start 9 characters after the prefix start ("__PARAMS[" is 9 chars)
var valueStart = prefixStart + 9;
var prefixEnd = txt.indexOf("]", valueStart);
if (prefixEnd != -1)
{
// Extract data string isolated inside the brackets: "Size,R,G,B,A"
var dataRaw = txt.substring(valueStart, prefixEnd);
var dataArr = dataRaw.split(",");
// Ensure all 5 properties exist in the sequence
if (dataArr.length >= 5) {
finalSize = Number(dataArr[0]);
var r = Number(dataArr[1]);
var g = Number(dataArr[2]);
var b = Number(dataArr[3]);
// Standard bitwise composition to map RGB array bytes to decimal color codes
finalColor = (r << 16) | (g << 8) | b;
// Dynamically assign the extracted Alpha value from the 5th element
finalAlpha = Number(dataArr[4]);
}
// Wipe the data tag out completely by starting right after the closing bracket "]"
cleanTxt = txt.substring(prefixEnd + 1);
}
}
setSubtitle(cleanTxt, finalPosition, finalColor, finalAlpha, false, finalSize);
}
This function serves as the Render Thread entry point. It:
- hides the conversation wheel,
- removes BioWare's HTML formatting tags,
- extracts subtitle formatting from the embedded
__PARAMS[...] payload when present,
- applies fallback formatting when it is absent, and
- forwards the processed data to
setSubtitle().
3. Extend setSubtitle()
Replace setSubtitle() with the implementation below.
function setSubtitle(sSubtitle, nPosition, nColor, nAlpha, bBackground, nSize)
{
ExternalInterface.Call("ASLog","CONV SET STRING: " + sSubtitle);
ExternalInterface.Call("ASLog","CONV SET SIZE: " + nSize);
var _loc7_;
var _loc8_;
var _loc9_;
var _loc10_;
var _loc11_;
var _loc12_;
var _loc13_;
if(sSubtitle == "")
{
HideSubtitle();
}
// Intercept if the string was passed from BioSFHandler_Conversation
else if (sSubtitle.indexOf("__PARAMS[") != -1)
{
var prefixStart = sSubtitle.indexOf("__PARAMS[");
var valueStart = prefixStart + 9;
var prefixEnd = sSubtitle.indexOf("]", valueStart);
if (prefixEnd != -1)
{
var dataRaw = sSubtitle.substring(valueStart, prefixEnd);
var dataArr = dataRaw.split(",");
if (dataArr.length >= 5) {
// Override incoming layout parameters with the hidden native data values
nSize = Number(dataArr[0]);
var r = Number(dataArr[1]);
var g = Number(dataArr[2]);
var b = Number(dataArr[3]);
nColor = (r << 16) | (g << 8) | b;
nAlpha = Number(dataArr[4]);
}
// Scrub the payload string clean to prevent UI leaks
sSubtitle = sSubtitle.substring(prefixEnd + 1);
}
// Re-route the cleaned text and data recursively into this function to execute the display block safely
setSubtitle(sSubtitle, nPosition, nColor, nAlpha, bBackground, nSize);
}
else
{
switch(nPosition)
{
case 0:
_loc7_ = SubtitleBottom;
_loc8_ = SubtitleTop;
_loc9_ = SubtitleConversation;
_loc10_ = loadingText;
break;
case 1:
_loc7_ = SubtitleTop;
_loc8_ = SubtitleBottom;
_loc9_ = SubtitleConversation;
_loc10_ = loadingText;
break;
case 2:
_loc7_ = SubtitleConversation;
_loc8_ = SubtitleBottom;
_loc9_ = SubtitleTop;
_loc10_ = loadingText;
break;
case 5:
_loc7_ = loadingText;
_loc8_ = SubtitleBottom;
_loc9_ = SubtitleTop;
_loc10_ = SubtitleConversation;
}
_loc7_._visible = true;
_loc11_ = _loc7_.SubtitleText.getTextFormat();
_loc11_.size = nSize;
_loc7_.SubtitleText.setTextFormat(_loc11_);
if(nPosition != 1)
{
if(bBackground)
{
_loc7_.gotoAndStop(2);
_loc12_ = _loc7_.SubtitleText._y + _loc7_.SubtitleText._height / 2;
com.TextFieldEx.SetText(_loc7_.SubtitleText,sSubtitle);
_loc7_.SubtitleText.autoSize = "center";
_loc7_.SubtitleText._y = _loc12_ - _loc7_.SubtitleText._height / 2;
if(ConversationWheel._visible)
{
_loc7_._y -= 120;
}
}
else
{
_loc7_.gotoAndStop(1);
com.TextFieldEx.SetText(_loc7_.SubtitleText,sSubtitle);
_loc7_.SubtitleText.autoSize = "center";
if(nPosition == 5)
{
_loc7_.SubtitleText.autoSize = "left";
}
else
{
_loc7_.SubtitleText.autoSize = "center";
}
_loc7_.SubtitleText._y = lstTextStartY[nPosition] - (_loc7_.SubtitleText._height - lstTextStartHeight[nPosition]);
}
}
else
{
_loc7_.SubtitleText._y = lstTextStartY[nPosition];
com.TextFieldEx.SetText(_loc7_.SubtitleText,sSubtitle);
_loc7_.SubtitleText.autoSize = "center";
}
_loc13_ = nAlpha * 0.39215686274509803;
_loc7_.SubtitleText.textColor = nColor;
_loc7_.SubtitleText._alpha = _loc13_;
_loc7_.SubtitleText._quality = "BEST";
_loc8_._visible = false;
_loc9_._visible = false;
_loc10_._visible = false;
}
}
The updated implementation serves as a protection against displaying __PARAMS[...] in FOVO triggered subtitles outside of FMVs by stripping it and calling itself again.
4. Update GUI_SF_Conversation.Conversation.gfx resources
Because GUI_SF_Conversation now assumes responsibility for both loading text and FMV subtitles, the loading text instance must be modified to match the layout previously provided by GUI_SF_LoadScreenToolTips. These adjustments preserve the original loading screen appearance while allowing the movie to serve both purposes.
A: Clone ..\other\ImportAssets2 (chid: 103, imp: "$Myriad") and update its properties as follows:
assets
└── tag[0] : U16 = 103, name[0] : String = "$BioMass"
This font is used by the "Loading" text.
B: Modify ..\frames\frame 1\PlaceObject2 (chid: 99, dpt: 71, nm: "loadingText") as follows:
matrix : MATRIX
├── hasScale : boolean = true
├── scaleX : FB[nScaleBits] = 1.0000457763671875
├── scaleY : FB[nScaleBits] = 1.0004119873046875
├── translateX : SB[nTranslateBits] = 1812
└── translateY : SB[nTranslateBits] = 12784
C: Disable transform matrix of ..\sprites\DefineSprite (chid: 99)\frame 1\PlaceObject2 (chid 98, dpt: 1, nm: "SubtitleText") as follows:
├── placeFlagHasMatrix : boolean = false
Steps B & C ensure the scaling and position match the original.
D: Rebind ..\texts\DefineEditText (chid 98) to $BioMass using Raw edit via the context menu on the DefineEditText and making the following change:
8. Preserve subtitle formatting through BioSeqAct_FaceOnlyVO
Extend BioSeqAct_FaceOnlyVO with the following UnrealScript implementation.
event function Activated()
{
if (m_eConvLine == EBioFOVOLines.FOVOLines_Unset)
{
SetSilent();
}
SetRTSubtitleParams();
Super(SequenceOp).Activated();
}
function SetSilent()
{
m_pSoundCue = None;
}
function SetRTSubtitleParams()
{
local BioSubtitles Subs;
Subs = BioWorldInfo(Class'Engine'.static.GetCurrentWorldInfo()).m_Subtitles;
if (Subs != None)
{
m_sSubtitle = "__PARAMS[" $ Subs.m_FontSize $ "," $ Subs.m_FontColor.R $ "," $ Subs.m_FontColor.G $ "," $ Subs.m_FontColor.B $ "," $ Subs.m_FontColor.A $ "]" $ m_sSubtitle;
}
}
Before dispatching the subtitle, the active formatting values are read from BioSubtitles and serialized into a hidden __PARAMS[...] prefix.
9. Add dedicated "silent" FaceOnlyVO for the Citadel arrival FMV
In BIOA_STA20_A_Arrival_CIN.TheWorld.PersistentLevel.Main_Sequence.ANIMCUTSCENE_STA_20_A_Arrival_SEQ04 via the Sequence Editor...
- Add a new
FaceOnlyVO object by cloning an existing one
- Hook
#275 Interp_1 from output PlaySEQ03 to the cloned FaceOnlyVO's Play input
- Hook the cloned
FaceOnlyVO's Pawn Variable to #111 BioInert_4 Invisible_Type
- In the cloned
FaceOnlyVO, change m_eConvLine to FOVOLines_Unset
- In the cloned
FaceOnlyVO, change m_aObjectComment[0] to Citadel Control, this is SSV Normandy requesting permission to land.
This FOVO will now play at the same time the FMV starts, causing the Render Thread subtitle to take over, making the transition seamless.
Known Limitation
One known edge case remains at the end of Eden Prime during the Prothean Beacon sequence.
Unlike most cinematics, this sequence does not use either BioConversation or FaceOnlyVO objects. Instead, subtitle timing is driven directly by the Interp in BIOA_PRO10_11_DSG.TheWorld.PersistentLevel.Main_Sequence.Combats.FinalFightOver.ANIMCUTSCENE_PRO_20_C_Artifact_male, meaning the subtitles still pass through the Render Thread path without the additional formatting data.
Resolving this would require restructuring the cinematic to introduce FaceOnlyVO events solely to drive subtitle generation. Given that this FMV is extremely brief and visually busy, the additional work is unlikely to justify the benefit.
Files: Startup_*.pcc, SFXGame.pcc
Needs fixed in localized file? Yes
Related issues:
Files modified/added for LE1 Community Patch:
LE1SubtitleFix-LE1CP.zip
Installation with M3 via LE1 Community Patch verified.
Background
In the original Mass Effect 1,
GUI_SF_Conversation.Conversationwas responsible for both the conversation wheel and subtitle rendering.In later games, BioWare split subtitle rendering into a separate native-only Render Thread UI (
FSFXGFx_RTGuiManager) while leaving the conversation wheel in the standard GFx UI.For Legendary Edition, this Render Thread system was backported into LE1. Rather than introducing a dedicated subtitle UI, BioWare repurposed
GUI_SF_LoadScreenToolTips.LoadScreenToolTips.As a result, this UI now performs two unrelated functions:
Because normal GFx UIs such as
GUI_SF_Conversation.Conversationare not rendered over FMVs, all FMV subtitles are routed through this Render Thread UI instead.This implementation introduces the issues described in #306.
In addition, the Render Thread UI only receives:
Consequently, FMV subtitles ignore the user's configured subtitle size (including launcher settings and custom launch parameters such as those provided by ME3Tweaks Mod Manager), causing issue #133.
For executable reverse engineering, search for the string
"setLoadingText"in Ghidra and inspect its references. The function that sends subtitle text toLoadScreenToolTipsinvokes the standard GFxInvokeMethod()API.Proposed Solution
Instead of rendering FMV subtitles through
GUI_SF_LoadScreenToolTips, redirect them back toGUI_SF_Conversation.Conversation, restoring a workflow much closer to the original Mass Effect 1 implementation.Because the Render Thread interface only exposes two parameters (text and subtitle/loading state), subtitle formatting data must be embedded into the transmitted string and reconstructed by the receiving Scaleform movie.
The implementation consists of:
GUI_SF_Conversation.Conversation.setLoadingText()entry point compatible with the Render Thread interface.__PARAMS[...]prefix.setSubtitle()to extract those parameters before rendering.BioSeqAct_FaceOnlyVOto prepend subtitle formatting information before dispatching subtitles.FaceOnlyVOobject to the Citadel arrival cinematic so its subtitles enter the Render Thread subtitle pipeline.This is the closest solution to restoring the behaviour seen in the original Mass Effect 1 without resorting to ASI modding.
Implementation
1. Redirect the Render Thread subtitle resource
Modify
FSFXGFx_RTGuiManagerinBIOUI.ini2. Add
setLoadingText()toStartup_*.GUI_SF_Conversation.ConversationThis function serves as the Render Thread entry point. It:
__PARAMS[...]payload when present,setSubtitle().3. Extend
setSubtitle()Replace
setSubtitle()with the implementation below.The updated implementation serves as a protection against displaying
__PARAMS[...]in FOVO triggered subtitles outside of FMVs by stripping it and calling itself again.4. Update
GUI_SF_Conversation.Conversation.gfxresourcesBecause
GUI_SF_Conversationnow assumes responsibility for both loading text and FMV subtitles, the loading text instance must be modified to match the layout previously provided byGUI_SF_LoadScreenToolTips. These adjustments preserve the original loading screen appearance while allowing the movie to serve both purposes.A: Clone
..\other\ImportAssets2 (chid: 103, imp: "$Myriad")and update its properties as follows:This font is used by the "Loading" text.
B: Modify
..\frames\frame 1\PlaceObject2 (chid: 99, dpt: 71, nm: "loadingText")as follows:C: Disable transform matrix of
..\sprites\DefineSprite (chid: 99)\frame 1\PlaceObject2 (chid 98, dpt: 1, nm: "SubtitleText")as follows:Steps B & C ensure the scaling and position match the original.
D: Rebind
..\texts\DefineEditText (chid 98)to$BioMassusingRaw editvia the context menu on theDefineEditTextand making the following change:8. Preserve subtitle formatting through
BioSeqAct_FaceOnlyVOExtend
BioSeqAct_FaceOnlyVOwith the following UnrealScript implementation.Before dispatching the subtitle, the active formatting values are read from
BioSubtitlesand serialized into a hidden__PARAMS[...]prefix.9. Add dedicated "silent"
FaceOnlyVOfor the Citadel arrival FMVIn
BIOA_STA20_A_Arrival_CIN.TheWorld.PersistentLevel.Main_Sequence.ANIMCUTSCENE_STA_20_A_Arrival_SEQ04via the Sequence Editor...FaceOnlyVOobject by cloning an existing one#275 Interp_1from outputPlaySEQ03to the clonedFaceOnlyVO'sPlayinputFaceOnlyVO'sPawnVariable to#111 BioInert_4 Invisible_TypeFaceOnlyVO, changem_eConvLinetoFOVOLines_UnsetFaceOnlyVO, changem_aObjectComment[0]toCitadel Control, this is SSV Normandy requesting permission to land.This FOVO will now play at the same time the FMV starts, causing the Render Thread subtitle to take over, making the transition seamless.
Known Limitation
One known edge case remains at the end of Eden Prime during the Prothean Beacon sequence.
Unlike most cinematics, this sequence does not use either
BioConversationorFaceOnlyVOobjects. Instead, subtitle timing is driven directly by theInterpinBIOA_PRO10_11_DSG.TheWorld.PersistentLevel.Main_Sequence.Combats.FinalFightOver.ANIMCUTSCENE_PRO_20_C_Artifact_male, meaning the subtitles still pass through the Render Thread path without the additional formatting data.Resolving this would require restructuring the cinematic to introduce
FaceOnlyVOevents solely to drive subtitle generation. Given that this FMV is extremely brief and visually busy, the additional work is unlikely to justify the benefit.