zx64 Posted February 6, 2016 Share Posted February 6, 2016 The dev build has a feature to load save games in replay mode: I did some digging around and threw together a quick UI mod to enable it in normal builds: Workshop Standalone download (I think this is the right folder to distribute) Link to comment Share on other sites More sharing options...
zx64 Posted February 7, 2016 Author Share Posted February 7, 2016 For others to learn from, here's a collection of notes on how I made this mod.The source is included in the standalone download in the OP, but I'll include bits for context.First was to find how much of replay mode was exposed to UnrealScript. > dir *replay* Volume in drive F has no label. Volume Serial Number is 9C8C-E0D4 Directory of F:\steam\steamapps\common\XCOM 2 SDK\Development\SrcOrig\XComGame\Classes 2016-02-05 03:09 6,874 UIReplay.uc 2016-02-05 03:09 4,721 XComMPReplayMgr.uc 2016-02-05 03:09 9,365 XComReplayMgr.uc UIReplay.uc isn't immediately useful, but we'll get back to that.Not concerned with MP, so let's have a look at XComReplayMgr.uc: /// <summary> /// Switches the running tactical game into a replay mode, where the visualization is driven by frames already in the /// game state history. /// </summary> simulated event StartReplay(int SessionStartStateIndex)Bingo. Now let's trace where that function is called: > findstr StartReplay * [...] X2TacticalGameRuleset.uc: // init the UI before StartReplay() because if its the Tutorial, Start replay will hide the UI which needs to have been created already X2TacticalGameRuleset.uc: XComTacticalGRI(class'WorldInfo'.static.GetWorldInfo().GRI).ReplayMgr.StartReplay(StartStateIndex); [...] Here's part of the function where those search results came up: if( `ONLINEEVENTMGR.bInitiateReplayAfterLoad ) { //Start the replay from the most recent start state StartStateIndex = CachedHistory.FindStartStateIndex(); FullGameState = CachedHistory.GetGameStateFromHistory(StartStateIndex, eReturnType_Copy, false); // init the UI before StartReplay() because if its the Tutorial, Start replay will hide the UI which needs to have been created already XComPlayerController(GetALocalPlayerController()).Pres.UIReplayScreen(); XComTacticalGRI(class'WorldInfo'.static.GetWorldInfo().GRI).ReplayMgr.StartReplay(StartStateIndex); `ONLINEEVENTMGR.bInitiateReplayAfterLoad = false;bInitiateReplayAfterLoad looks promising, let's find where that's set to true: > findstr /C:"bInitiateReplayAfterLoad = true" * UIDebugChallengeMode.uc: `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; UIShell.uc: `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; UIShell.uc: `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; UIShell.uc: `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; UIShellDifficulty.uc: `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; XComCheatManager.uc: `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true;UIShell.uc is where the debug main menu is set up and this is where we find the "Load Replay" button's implementation: // Button callbacks simulated function OnMenuButtonClicked(UIButton button) { [...] switch( button.MCName ) { [...] case 'Special': `XCOMHISTORY.ResetHistory(); `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; XComShellPresentationLayer(Owner).UILoadScreen(); break;So all I have to do is create a new button that does the same thing.Fortunately there's some documentation on this at: XCOM 2 SDK\Documentation\Tech\XCOM2Mods_UserInterface.pdf"Manipulating current UI elements" is exactly what we're after, so let's follow along with the example and extend a UIScreenListener and attach it to the main menu. class LoadReplay_ZX extends UIScreenListener; event OnInit(UIScreen Screen) { } defaultproperties { ScreenClass = ??? } But hang on, we don't yet know the ScreenClass for the non-debug main menu.As it happens, the debug main menu has a button that switches back to the final menu: Button = Spawn(class'UIButton', DebugMenuContainer); Button.InitButton('FinalShellMenu', m_sFinalShellDebug, UIDebugButtonClicked); [...] case 'FinalShellMenu': XComShellPresentationLayer(Owner).UIFinalShellScreen(); break;and there's a UIFinalShell.uc, so let's go with that: defaultproperties { ScreenClass = class'UIFinalShell'; }Now here's where I ran into a few fun extra challenges...You see that Spawn in the example and just above? That didn't compile for my UIScreenListener for some reason.Now, UIShell.uc etc. is actually a UIScreen rather than a UIScreenListener, so maybe wecan call Spawn using the Screen object that's passed into our OnInit event handler. event OnInit(UIScreen Screen) { local UIButton Loadreplay; Loadreplay = Screen.Spawn(class'UIButton', Screen); Loadreplay.InitButton('LoadReplay_ZX', "Load Replay", OnLoadReplay); Loadreplay.SetPosition(0,0); }Something to watch out here: UnrealScript has two separate types, Names and Strings.Names use single quotes and Strings use double quotes.This caught me out as I was getting a compile error when writing e.g.: Loadreplay.InitButton("LoadReplay_ZX", "Load Replay", OnLoadReplay); I had to dig through UIButton.uc before realising that single vs double quotes was significant!So all we have to do now is copy that replay code from earlier into our OnLoadReplay, right? simulated function OnLoadReplay(UIButton button) { `XCOMHISTORY.ResetHistory(); `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; XComShellPresentationLayer(Owner).UILoadScreen(); } Warning/Error Summary --------------------- F:\steam\steamapps\common\XCOM 2 SDK\Development\Src\LoadReplay\Classes\LoadReplay_ZX.uc(21) : Error, 'XComShellPresentationLayer': Bad command or expressionDamn! I think this is another UIScreen vs UIScreenListener thing, but even if I store the UIScreen reference, it still doesn't compile: var UIFinalShell Shell; event OnInit(UIScreen Screen) { [...] Shell = UIFinalShell(Screen); } simulated function OnLoadReplay(UIButton button) { `XCOMHISTORY.ResetHistory(); `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; Shell.XComShellPresentationLayer(Owner).UILoadScreen(); } Warning/Error Summary --------------------- F:\steam\steamapps\common\XCOM 2 SDK\Development\Src\LoadReplay\Classes\LoadReplay_ZX.uc(22) : Error, Unrecognized member 'XComShellPresentationLayer' in class 'UIFinalShell' So now we have to do more source digging. What does that UILoadScreen method do? > findstr "UILoadScreen" * UICombatLose.uc: Movie.Pres.UILoadScreen(); UIFinalShell.uc: XComShellPresentationLayer(Owner).UILoadScreen(); UILoadScreenAnimation.uc:// FILE: UILoadScreenAnimation.uc UILoadScreenAnimation.uc:class UILoadScreenAnimation extends UIScreen; UIPauseMenu.uc: Movie.Pres.UILoadScreen(); UIShell.uc: XComShellPresentationLayer(Owner).UILoadScreen(); UIShell.uc: XComShellPresentationLayer(Owner).UILoadScreen(); UIShellStrategy.uc: XComShellPresentationLayer(Owner).UILoadScreen(); XComPresentationLayerBase.uc:var protected UILoadScreenAnimation m_kLoadAnimation; XComPresentationLayerBase.uc:simulated function UILoadScreen() XComPresentationLayerBase.uc: HandleInvalidStorage(m_strSelectSaveDeviceForLoadPrompt, UILoadScreen); XComPresentationLayerBase.uc: m_kLoadAnimation = Spawn( class'UILoadScreenAnimation', self );So let's take a peek in XComPresentationLayerBase.uc: simulated function UILoadScreen() { local UIMovie TargetMovie; TargetMovie = XComShellPresentationLayer(self) == none ? Get2DMovie() : Get3DMovie(); if( `ONLINEEVENTMGR.HasValidLoginAndStorage() ) { ScreenStack.Push( Spawn( class'UILoadGame', self ), TargetMovie ); } else { HandleInvalidStorage(m_strSelectSaveDeviceForLoadPrompt, UILoadScreen); // Failed to enter state } }Hmm, nothing that obvious, but wait a second, what's this Movie.Pres thing in the pause menu?At this point, I figured it was worth a try and plugged it into my OnLoadReplay: simulated function OnLoadReplay(UIButton button) { `XCOMHISTORY.ResetHistory(); `ONLINEEVENTMGR.bInitiateReplayAfterLoad = true; Shell.Movie.Pres.UILoadScreen(); } Success!Movie though? What's up with that?As noted in UserInterface.pdf, XCOM uses Flash for the UI (you might recognise the name "Scaleform" from various logo splashes). I expect the term "Movie" dates way back to their Flashplayer and Shockwave days and they've never felt the need to rename the concept to reflect the more general use of that technology.Finally, I wanted to streamline the replay UI with another button that hid the UI and started automatic playback.This means a new UIScreenListener, this time attached to UIReplay.I'd spotted a ConsoleCommand function being used in a variety of places, so figured I'd just need to do: ConsoleCommand("ReplayToggleUI"); ConsoleCommand("ReplayAutoplay"); but again, not available to UIScreenListener.Instead I went digging for the source of those console commands and copied over what those commands did: simulated function OnAutoPlay(UIButton button) { `REPLAY.ToggleUI(); XComTacticalGRI(class'WorldInfo'.static.GetWorldInfo().GRI).ReplayMgr.StepReplayAll(); } Other important thing, remember to update XComEngine.ini to include the new class: [Engine.ScriptPackages] +NonNativePackages=LoadReplay [Engine.Engine] +ModClassOverrides=(BaseGameClass="UIScreenListener", ModClass="LoadReplay_ZX") +ModClassOverrides=(BaseGameClass="UIScreenListener", ModClass="ReplayUITweaks_ZX") Final aside, I had to edit my x2proj by hand to get it to add the new ReplayUITweaks_ZX.uc to the right location, VS kept insisting on adding it to the root of the project. And that's it! Hope you learned something. :-) Link to comment Share on other sites More sharing options...
zx64 Posted February 8, 2016 Author Share Posted February 8, 2016 Updated to fix graphical glitching when in mission. Root cause was me storing a reference to the UI Shell inside my class.Changing it to store a reference to the UIMovie seems to resolve the issue. // Do not store a reference to UIScreen here, it causes graphical glitching in game var UIMovie Movie; event OnInit(UIScreen Screen) { // Need this to activate the load save dialog Movie = Screen.Movie; ... } simulated function OnLoadReplay(UIButton button) { ... Movie.Pres.UILoadScreen(); } Also as I learned from reading through the LWS mods, you don't need ModClassOverrides entries for UIScreenListeners. Link to comment Share on other sites More sharing options...
Recommended Posts