diff --git a/source/Launcher/Enderal Launcher.au3 b/source/Launcher/Enderal Launcher.au3 index c26b7a4fd..ed78e3888 100644 --- a/source/Launcher/Enderal Launcher.au3 +++ b/source/Launcher/Enderal Launcher.au3 @@ -10,7 +10,18 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include #include @@ -33,6 +44,22 @@ Global Const $REG_START_STEAM = "Start_Steam" Global Const $AA_TAA = "TAA (Best quality)" Global Const $AA_FXAA = "FXAA (Low)" Global Const $AA_OFF = "Off (Best performance)" +; Plugin load order classes - the engine always loads masters before regular plugins +Global Const $PLUGIN_CLASS_MASTER = 1 +Global Const $PLUGIN_CLASS_NORMAL = 2 +Global Const $PLUGIN_FLAG_MASTER = 0x00000001 +Global Const $PLUGIN_FLAG_LIGHT = 0x00000200 +; Check box images of the plugin list (state image indexes) +Global Const $PLUGIN_STATE_UNCHECKED = 1 +Global Const $PLUGIN_STATE_CHECKED = 2 +Global Const $PLUGIN_STATE_UNCHECKED_DIMMED = 3 +Global Const $PLUGIN_STATE_CHECKED_DIMMED = 4 +; Theme parts used to draw them (BUTTON class) +Global Const $THEME_BP_CHECKBOX = 3 +Global Const $THEME_CB_UNCHECKED = 1 +Global Const $THEME_CB_UNCHECKED_DISABLED = 4 +Global Const $THEME_CB_CHECKED = 5 +Global Const $THEME_CB_CHECKED_DISABLED = 8 Global $PLUGINS_DIR = "" Global $PLUGINS_FILE = "" Global $DOCUMENTS_DIR = "" @@ -71,9 +98,28 @@ Global $chkController, $chkVibration Global $chkStartSteam Global $grpIniFiles, $btnOpenEnderalIni, $btnOpenEnderalPrefsIni, $btnOpenCustomIni, $btnOpenModIni, $btnOpenSavegames +; Plugins tab controls +Global $lvPlugins, $hLvPlugins = 0 +Global $btnPluginUp, $btnPluginDown, $btnPluginsEnableAll, $btnPluginsDisableAll +Global $lblPluginsCount, $lblPluginsHint +Global $hPluginStateImages = 0 + +; Plugin list state +; [i][0] = file name, [i][1] = enabled, [i][2] = locked (always loaded by the game), +; [i][3] = load order class, [i][4] = type label, [i][5] = present in plugins.txt +Global $aPluginList[1][6] +Global $iPluginListCount = 0 +Global $bPluginListLoaded = False +Global $bPluginListDirty = False +Global $bPluginListUpdating = False +Global $bPluginListReadOnly = False + ; Steam version detection Global $bIsSteamVersion = False +; Mod Organizer 2 directory, empty when the launcher was not started by MO2 +Global $MO2_DIR = "" + ; ============================================ ; Helper Functions ; ============================================ @@ -562,6 +608,9 @@ Func Main() ; Register message handler for slider updates GUIRegisterMsg($WM_HSCROLL, "WM_HSCROLL") + ; Register message handler for plugin list check boxes + GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY") + ; Main loop MainLoop() @@ -586,6 +635,80 @@ Func WM_HSCROLL($hWnd, $iMsg, $wParam, $lParam) Return $GUI_RUNDEFMSG EndFunc +Func WM_NOTIFY($hWnd, $iMsg, $wParam, $lParam) + ; Handles the plugin list: check box changes send no GUIGetMsg event, and rows that + ; cannot be changed are drawn dimmed + If $hLvPlugins = 0 Then Return $GUI_RUNDEFMSG + + Local $tNMHDR = DllStructCreate($tagNMHDR, $lParam) + If @error Then Return $GUI_RUNDEFMSG + If HWnd(DllStructGetData($tNMHDR, "hWndFrom")) <> $hLvPlugins Then Return $GUI_RUNDEFMSG + + Switch DllStructGetData($tNMHDR, "Code") + Case $NM_CUSTOMDRAW + Return DrawPluginRow($lParam) + Case $LVN_ITEMCHANGING, $LVN_ITEMCHANGED + Return OnPluginStateChange(DllStructGetData($tNMHDR, "Code"), $lParam) + EndSwitch + + Return $GUI_RUNDEFMSG +EndFunc + +Func DrawPluginRow($lParam) + Local $tCustomDraw = DllStructCreate($tagNMLVCUSTOMDRAW, $lParam) + If @error Then Return $CDRF_DODEFAULT + + Switch DllStructGetData($tCustomDraw, "dwDrawStage") + Case $CDDS_PREPAINT + Return $CDRF_NOTIFYITEMDRAW + Case $CDDS_ITEMPREPAINT + Local $iItem = DllStructGetData($tCustomDraw, "dwItemSpec") + If $iItem >= 0 And $iItem < $iPluginListCount And IsPluginRowFixed($iItem) Then + DllStructSetData($tCustomDraw, "clrText", _WinAPI_GetSysColor($COLOR_GRAYTEXT)) + EndIf + Return $CDRF_NEWFONT + EndSwitch + + Return $CDRF_DODEFAULT +EndFunc + +Func OnPluginStateChange($iCode, $lParam) + If $bPluginListUpdating Then Return $GUI_RUNDEFMSG + + Local $tNMLV = DllStructCreate($tagNMLISTVIEW, $lParam) + If @error Then Return $GUI_RUNDEFMSG + If Not BitAND(DllStructGetData($tNMLV, "Changed"), $LVIF_STATE) Then Return $GUI_RUNDEFMSG + + Local $iItem = DllStructGetData($tNMLV, "Item") + Local $iOldCheck = BitShift(BitAND(DllStructGetData($tNMLV, "OldState"), $LVIS_STATEIMAGEMASK), 12) + Local $iNewCheck = BitShift(BitAND(DllStructGetData($tNMLV, "NewState"), $LVIS_STATEIMAGEMASK), 12) + + ; A check state change has both state image indexes set + Local $bCheckChanged = ($iOldCheck <> 0 And $iNewCheck <> 0 And $iOldCheck <> $iNewCheck) + Local $bValidItem = ($iItem >= 0 And $iItem < $iPluginListCount) + + If $iCode = $LVN_ITEMCHANGING Then + ; Rows that cannot be changed keep their state + If $bCheckChanged And $bValidItem And IsPluginRowFixed($iItem) Then Return True + Return $GUI_RUNDEFMSG + EndIf + + If $bCheckChanged And $bValidItem Then + If IsPluginRowFixed($iItem) Then + ; Safety net in case the rejection above did not take effect + $bPluginListUpdating = True + SetPluginRowState($iItem) + $bPluginListUpdating = False + Else + $aPluginList[$iItem][1] = ($iNewCheck = $PLUGIN_STATE_CHECKED) + $bPluginListDirty = True + EndIf + EndIf + + UpdatePluginButtons() + Return $GUI_RUNDEFMSG +EndFunc + ; ============================================ ; Startup Checks ; ============================================ @@ -602,6 +725,11 @@ Func StartupChecks() ; Detect Steam version by checking for steam_api64.dll $bIsSteamVersion = FileExists($LAUNCHER_DIR & "\steam_api64.dll") + ; Detect Mod Organizer 2 while its process is still our parent. + ; Under MO2 the load order belongs to the active MO2 profile, so the launcher must not touch it. + $MO2_DIR = GetMO2Directory() + $bPluginListReadOnly = ($MO2_DIR <> "") + CheckRegistryPaths() CheckPluginsFile() CheckINIFiles() @@ -633,8 +761,24 @@ Func CheckRegistryPaths() EndIf EndFunc +Func GetRequiredPlugins() + ; Plugins Enderal cannot run without - always kept enabled + Local $aRequired[2] = ["Enderal - Forgotten Stories.esm", "SkyUI_SE.esp"] + Return $aRequired +EndFunc + Func CheckPluginsFile() - Local $requiredLines[2] = ["*Enderal - Forgotten Stories.esm", "*SkyUI_SE.esp"] + Local $aRequired = GetRequiredPlugins() + Local $requiredLines[UBound($aRequired)] + Local $iRequired = 0 + + ; Only enforce required plugins that are installed (SkyUI may be absent) + For $i = 0 To UBound($aRequired) - 1 + If Not FileExists($LAUNCHER_DIR & "\Data\" & $aRequired[$i]) Then ContinueLoop + $requiredLines[$iRequired] = "*" & $aRequired[$i] + $iRequired += 1 + Next + ReDim $requiredLines[$iRequired] If Not DirExists($PLUGINS_DIR) Then DirCreate($PLUGINS_DIR) @@ -1036,6 +1180,10 @@ Func CreateSettingsWindow() GUICtrlCreateTabItem("General") CreateControlsControls() + ; Plugins tab + GUICtrlCreateTabItem("Plugins") + CreatePluginsControls() + ; End tab items GUICtrlCreateTabItem("") @@ -1146,6 +1294,44 @@ Func CreateControlsControls() GUICtrlCreateGroup("", -99, -99, 1, 1) EndFunc +Func CreatePluginsControls() + Local $leftMargin = $TAB_LEFT_MARGIN + Local $topMargin = 58 + Local $listWidth = 560 + Local $listHeight = 340 + Local $sideX = $leftMargin + $listWidth + 20 + Local $sideWidth = 198 + Local $btnHeight = 32 + + $lvPlugins = GUICtrlCreateListView("Plugin|Type", $leftMargin, $topMargin, $listWidth, $listHeight, _ + BitOR($LVS_REPORT, $LVS_SINGLESEL, $LVS_SHOWSELALWAYS, $LVS_NOSORTHEADER), _ + BitOR($LVS_EX_CHECKBOXES, $LVS_EX_FULLROWSELECT, $LVS_EX_DOUBLEBUFFER)) + $hLvPlugins = GUICtrlGetHandle($lvPlugins) + _GUICtrlListView_SetColumnWidth($hLvPlugins, 0, 420) + _GUICtrlListView_SetColumnWidth($hLvPlugins, 1, 120) + CreatePluginStateImages() + + $btnPluginUp = GUICtrlCreateButton("Move Up", $sideX, $topMargin, $sideWidth, $btnHeight) + GUICtrlSetState(-1, $GUI_DISABLE) + $btnPluginDown = GUICtrlCreateButton("Move Down", $sideX, $topMargin + 38, $sideWidth, $btnHeight) + GUICtrlSetState(-1, $GUI_DISABLE) + $btnPluginsEnableAll = GUICtrlCreateButton("Enable All", $sideX, $topMargin + 90, $sideWidth, $btnHeight) + $btnPluginsDisableAll = GUICtrlCreateButton("Disable All", $sideX, $topMargin + 128, $sideWidth, $btnHeight) + + $lblPluginsCount = GUICtrlCreateLabel("", $sideX, $topMargin + 178, $sideWidth, 20) + + Local $hint = "Masters (ESM/ESL) always load before regular plugins." & @CRLF & @CRLF & _ + "Plugins Enderal requires are loaded by the game itself and cannot be changed." + If $bPluginListReadOnly Then + $hint = "The load order belongs to the active Mod Organizer profile." & @CRLF & @CRLF & _ + "Enable, disable and sort plugins in Mod Organizer instead." + EndIf + + $lblPluginsHint = GUICtrlCreateLabel($hint, $sideX, $topMargin + 206, $sideWidth, 134) + GUICtrlSetFont(-1, 8) + GUICtrlSetColor(-1, 0x606060) +EndFunc + Func PopulateResolutions() Local $aResolutions = GetSupportedResolutions() Local $resolutions = "" @@ -1229,6 +1415,494 @@ Func GetSupportedResolutions() Return $aResult EndFunc +; ============================================ +; Plugin Management (plugins.txt) +; ============================================ +Func GetPluginHeaderFlags($filePath) + ; Reads the record flags of the plugin's TES4 header + Local $hFile = FileOpen($filePath, $FO_BINARY) + If $hFile = -1 Then Return 0 + + Local $header = FileRead($hFile, 12) + FileClose($hFile) + If BinaryLen($header) < 12 Then Return 0 + If BinaryToString(BinaryMid($header, 1, 4)) <> "TES4" Then Return 0 + + ; Record flags are a little endian dword at offset 8 + Local $hex = StringTrimLeft(String(BinaryMid($header, 9, 4)), 2) + Return Dec(StringMid($hex, 7, 2) & StringMid($hex, 5, 2) & StringMid($hex, 3, 2) & StringMid($hex, 1, 2), 2) +EndFunc + +Func GetPluginTypeInfo($filePath, $fileName) + ; Returns [load order class, type label] + Local $ext = StringLower(StringRight($fileName, 4)) + Local $flags = GetPluginHeaderFlags($filePath) + Local $bMaster = BitAND($flags, $PLUGIN_FLAG_MASTER) <> 0 Or $ext = ".esm" Or $ext = ".esl" + Local $bLight = BitAND($flags, $PLUGIN_FLAG_LIGHT) <> 0 Or $ext = ".esl" + + Local $aInfo[2] + $aInfo[0] = $bMaster ? $PLUGIN_CLASS_MASTER : $PLUGIN_CLASS_NORMAL + If $bLight Then + $aInfo[1] = "Light" + ElseIf $bMaster Then + $aInfo[1] = "Master" + Else + $aInfo[1] = "Plugin" + EndIf + Return $aInfo +EndFunc + +Func ScanDataPlugins() + ; All plugin files present in the Data directory, sorted by name + ; [i][0] = file name, [i][1] = load order class, [i][2] = type label + Local $dataDir = $LAUNCHER_DIR & "\Data" + Local $aFiles[64][3] + Local $iCount = 0 + + Local $hSearch = FileFindFirstFile($dataDir & "\*.es*") + If $hSearch <> -1 Then + While 1 + Local $fileName = FileFindNextFile($hSearch) + If @error Then ExitLoop + If @extended Then ContinueLoop ; skip directories + + Local $ext = StringLower(StringRight($fileName, 4)) + If $ext <> ".esm" And $ext <> ".esp" And $ext <> ".esl" Then ContinueLoop + + If $iCount >= UBound($aFiles) Then ReDim $aFiles[$iCount + 64][3] + + Local $aInfo = GetPluginTypeInfo($dataDir & "\" & $fileName, $fileName) + $aFiles[$iCount][0] = $fileName + $aFiles[$iCount][1] = $aInfo[0] + $aFiles[$iCount][2] = $aInfo[1] + $iCount += 1 + WEnd + FileClose($hSearch) + EndIf + + ReDim $aFiles[$iCount][3] + If $iCount > 1 Then _ArraySort($aFiles, 0, 0, 0, 0) + Return $aFiles +EndFunc + +Func ReadPluginsFile() + ; plugins.txt entries in file order: [i][0] = file name, [i][1] = enabled + Local $aListed[1][2] + Local $iCount = 0 + + If FileExists($PLUGINS_FILE) Then + Local $hFile = FileOpen($PLUGINS_FILE, BitOR($FO_READ, $FO_UTF8)) + If $hFile <> -1 Then + Local $content = FileRead($hFile) + FileClose($hFile) + + ; Drop a byte order mark left over from other tools + If StringLeft($content, 1) = ChrW(65279) Then $content = StringTrimLeft($content, 1) + + Local $aLines = StringSplit(StringStripCR($content), @LF) + ReDim $aListed[$aLines[0] + 1][2] + + For $i = 1 To $aLines[0] + Local $line = StringStripWS($aLines[$i], 3) + If $line = "" Or StringLeft($line, 1) = "#" Then ContinueLoop + + Local $bEnabled = (StringLeft($line, 1) = "*") + If $bEnabled Then $line = StringStripWS(StringTrimLeft($line, 1), 3) + If $line = "" Then ContinueLoop + + $aListed[$iCount][0] = $line + $aListed[$iCount][1] = $bEnabled + $iCount += 1 + Next + EndIf + EndIf + + ReDim $aListed[$iCount][2] + Return $aListed +EndFunc + +Func GetCCCPlugins() + ; Skyrim.ccc entries - the game loads these on its own whenever the files exist + Local $aEntries[1] + Local $iCount = 0 + Local $cccFile = $LAUNCHER_DIR & "\Skyrim.ccc" + + If FileExists($cccFile) Then + Local $aLines = StringSplit(StringStripCR(FileRead($cccFile)), @LF) + ReDim $aEntries[$aLines[0] + 1] + + For $i = 1 To $aLines[0] + Local $line = StringStripWS($aLines[$i], 3) + If $line = "" Or StringLeft($line, 1) = "#" Then ContinueLoop + $aEntries[$iCount] = $line + $iCount += 1 + Next + EndIf + + ReDim $aEntries[$iCount] + Return $aEntries +EndFunc + +Func GetImplicitPlugins() + ; Plugins the game loads by itself, in the order it loads them: the Skyrim masters, + ; the Skyrim.ccc entries, then the plugins Enderal requires + Local $aMasters[5] = ["Skyrim.esm", "Update.esm", "Dawnguard.esm", "HearthFires.esm", "Dragonborn.esm"] + Local $aCCC = GetCCCPlugins() + Local $aRequired = GetRequiredPlugins() + + Local $aImplicit[UBound($aMasters) + UBound($aCCC) + UBound($aRequired)] + Local $iCount = 0 + + For $i = 0 To UBound($aMasters) - 1 + $aImplicit[$iCount] = $aMasters[$i] + $iCount += 1 + Next + For $i = 0 To UBound($aCCC) - 1 + $aImplicit[$iCount] = $aCCC[$i] + $iCount += 1 + Next + For $i = 0 To UBound($aRequired) - 1 + $aImplicit[$iCount] = $aRequired[$i] + $iCount += 1 + Next + + Return $aImplicit +EndFunc + +Func IsRequiredPlugin($fileName) + Local $aRequired = GetRequiredPlugins() + For $i = 0 To UBound($aRequired) - 1 + If $aRequired[$i] = $fileName Then Return True + Next + Return False +EndFunc + +Func FindPluginFile(ByRef $aFiles, $fileName) + For $i = 0 To UBound($aFiles) - 1 + If $aFiles[$i][0] = $fileName Then Return $i + Next + Return -1 +EndFunc + +Func FindListedPlugin(ByRef $aListed, $fileName) + For $i = 0 To UBound($aListed) - 1 + If $aListed[$i][0] = $fileName Then Return $i + Next + Return -1 +EndFunc + +Func FindPluginRow(ByRef $aList, $iCount, $fileName) + For $i = 0 To $iCount - 1 + If $aList[$i][0] = $fileName Then Return $i + Next + Return -1 +EndFunc + +Func AddPluginRow(ByRef $aList, ByRef $iCount, ByRef $aFiles, $iFile, $bEnabled, $bLocked, $bListed) + $aList[$iCount][0] = $aFiles[$iFile][0] + $aList[$iCount][1] = $bEnabled + $aList[$iCount][2] = $bLocked + $aList[$iCount][3] = $aFiles[$iFile][1] + $aList[$iCount][4] = $bLocked ? $aFiles[$iFile][2] & " (fixed)" : $aFiles[$iFile][2] + $aList[$iCount][5] = $bListed + $iCount += 1 +EndFunc + +Func LoadPluginList() + ; Keep unsaved changes when the settings are reloaded (e.g. after applying a detail preset) + If $bPluginListDirty Then Return + + Local $aFiles = ScanDataPlugins() + Local $aListed = ReadPluginsFile() + Local $aImplicit = GetImplicitPlugins() + Local $iFileCount = UBound($aFiles) + + Local $aList[$iFileCount + 1][6] + Local $iCount = 0 + Local $iFile + + ; Plugins loaded by the game itself come first and cannot be changed + For $i = 0 To UBound($aImplicit) - 1 + $iFile = FindPluginFile($aFiles, $aImplicit[$i]) + If $iFile < 0 Then ContinueLoop ; not installed + If FindPluginRow($aList, $iCount, $aFiles[$iFile][0]) >= 0 Then ContinueLoop + ; The plugins Enderal requires are always written back to plugins.txt as enabled, + ; the remaining ones only when they were listed there before + Local $bListed = IsRequiredPlugin($aFiles[$iFile][0]) Or FindListedPlugin($aListed, $aFiles[$iFile][0]) >= 0 + AddPluginRow($aList, $iCount, $aFiles, $iFile, True, True, $bListed) + Next + + ; Then the plugins.txt entries in their current order + For $i = 0 To UBound($aListed) - 1 + $iFile = FindPluginFile($aFiles, $aListed[$i][0]) + If $iFile < 0 Then ContinueLoop ; entry without a file - drop it + If FindPluginRow($aList, $iCount, $aFiles[$iFile][0]) >= 0 Then ContinueLoop + AddPluginRow($aList, $iCount, $aFiles, $iFile, $aListed[$i][1], False, True) + Next + + ; Finally the plugins that are not in plugins.txt yet, masters first + For $iClass = $PLUGIN_CLASS_MASTER To $PLUGIN_CLASS_NORMAL + For $i = 0 To $iFileCount - 1 + If $aFiles[$i][1] <> $iClass Then ContinueLoop + If FindPluginRow($aList, $iCount, $aFiles[$i][0]) >= 0 Then ContinueLoop + AddPluginRow($aList, $iCount, $aFiles, $i, False, False, False) + Next + Next + + ; Keep masters above regular plugins - the engine loads them that way regardless of plugins.txt + Local $aSorted[$iFileCount + 1][6] + Local $iSorted = 0 + For $iRank = 0 To 2 + For $i = 0 To $iCount - 1 + Local $iRowRank = $aList[$i][2] ? 0 : ($aList[$i][3] = $PLUGIN_CLASS_MASTER ? 1 : 2) + If $iRowRank <> $iRank Then ContinueLoop + For $c = 0 To 5 + $aSorted[$iSorted][$c] = $aList[$i][$c] + Next + $iSorted += 1 + Next + Next + + $aPluginList = $aSorted + $iPluginListCount = $iSorted + $bPluginListLoaded = True + + FillPluginListView() +EndFunc + +Func CreatePluginStateImages() + ; Check box images for the plugin list: the two extra dimmed ones mark rows that cannot be changed. + ; Without visual styles the list keeps the check boxes the control provides itself. + If Not _WinAPI_IsThemeActive() Then Return + + Local $hTheme = _WinAPI_OpenThemeData($hLvPlugins, "BUTTON") + If $hTheme = 0 Then Return + + Local $hWndDC = _WinAPI_GetDC($hLvPlugins) + Local $tSize = _WinAPI_GetThemePartSize($hTheme, $THEME_BP_CHECKBOX, $THEME_CB_UNCHECKED, $hWndDC, 0, $TS_TRUE) + Local $iError = @error + _WinAPI_ReleaseDC($hLvPlugins, $hWndDC) + + Local $iWidth = 16, $iHeight = 16 + If Not $iError Then + $iWidth = DllStructGetData($tSize, "X") + $iHeight = DllStructGetData($tSize, "Y") + EndIf + + If $iWidth >= 8 And $iHeight >= 8 Then + Local $hBitmap = CreateTransparentBitmap($iWidth * 4, $iHeight) + If $hBitmap <> 0 Then + ; Draw the four states next to each other, in state image index order + Local $aStates[4] = [$THEME_CB_UNCHECKED, $THEME_CB_CHECKED, _ + $THEME_CB_UNCHECKED_DISABLED, $THEME_CB_CHECKED_DISABLED] + Local $hMemDC = _WinAPI_CreateCompatibleDC(0) + Local $hPrevBitmap = _WinAPI_SelectObject($hMemDC, $hBitmap) + Local $tRect = DllStructCreate($tagRECT) + DllStructSetData($tRect, "Top", 0) + DllStructSetData($tRect, "Bottom", $iHeight) + + For $i = 0 To UBound($aStates) - 1 + DllStructSetData($tRect, "Left", $i * $iWidth) + DllStructSetData($tRect, "Right", ($i + 1) * $iWidth) + _WinAPI_DrawThemeBackground($hTheme, $THEME_BP_CHECKBOX, $aStates[$i], $hMemDC, $tRect) + Next + + _WinAPI_SelectObject($hMemDC, $hPrevBitmap) + _WinAPI_DeleteDC($hMemDC) + + $hPluginStateImages = _GUIImageList_Create($iWidth, $iHeight, 5, 0, 4, 4) + If $hPluginStateImages <> 0 Then + _GUIImageList_Add($hPluginStateImages, $hBitmap) + _GUICtrlListView_SetImageList($hLvPlugins, $hPluginStateImages, 2) ; 2 = LVSIL_STATE + EndIf + + _WinAPI_DeleteObject($hBitmap) + EndIf + EndIf + + _WinAPI_CloseThemeData($hTheme) +EndFunc + +Func CreateTransparentBitmap($iWidth, $iHeight) + ; Top down 32 bit device independent bitmap, cleared to fully transparent + Local $tBitmapInfo = DllStructCreate("dword Size;long Width;long Height;ushort Planes;ushort BitCount;" & _ + "dword Compression;dword SizeImage;long XPelsPerMeter;long YPelsPerMeter;dword ClrUsed;dword ClrImportant") + DllStructSetData($tBitmapInfo, "Size", DllStructGetSize($tBitmapInfo)) + DllStructSetData($tBitmapInfo, "Width", $iWidth) + DllStructSetData($tBitmapInfo, "Height", -$iHeight) + DllStructSetData($tBitmapInfo, "Planes", 1) + DllStructSetData($tBitmapInfo, "BitCount", 32) + DllStructSetData($tBitmapInfo, "Compression", 0) ; BI_RGB + + Local $aCall = DllCall("gdi32.dll", "handle", "CreateDIBSection", "handle", 0, "struct*", $tBitmapInfo, _ + "uint", 0, "ptr*", 0, "handle", 0, "dword", 0) + If @error Or Not IsArray($aCall) Then Return 0 + Return $aCall[0] +EndFunc + +Func IsPluginRowFixed($iIndex) + ; True when the row cannot be toggled: plugins the game loads itself, or MO2 owning the load order + If $bPluginListReadOnly Then Return True + Return $aPluginList[$iIndex][2] +EndFunc + +Func SetPluginRowState($iIndex) + If $hPluginStateImages = 0 Then + ; No dimmed images available - fall back to the plain check box + _GUICtrlListView_SetItemChecked($hLvPlugins, $iIndex, $aPluginList[$iIndex][1]) + Return + EndIf + + Local $iState + If IsPluginRowFixed($iIndex) Then + $iState = $aPluginList[$iIndex][1] ? $PLUGIN_STATE_CHECKED_DIMMED : $PLUGIN_STATE_UNCHECKED_DIMMED + Else + $iState = $aPluginList[$iIndex][1] ? $PLUGIN_STATE_CHECKED : $PLUGIN_STATE_UNCHECKED + EndIf + + _GUICtrlListView_SetItemState($hLvPlugins, $iIndex, BitShift($iState, -12), $LVIS_STATEIMAGEMASK) +EndFunc + +Func FillPluginListView() + $bPluginListUpdating = True + _GUICtrlListView_BeginUpdate($hLvPlugins) + _GUICtrlListView_DeleteAllItems($hLvPlugins) + + For $i = 0 To $iPluginListCount - 1 + _GUICtrlListView_AddItem($hLvPlugins, $aPluginList[$i][0]) + _GUICtrlListView_AddSubItem($hLvPlugins, $i, $aPluginList[$i][4], 1) + SetPluginRowState($i) + Next + + _GUICtrlListView_EndUpdate($hLvPlugins) + $bPluginListUpdating = False + + UpdatePluginColumnWidths() + UpdatePluginButtons() +EndFunc + +Func UpdatePluginColumnWidths() + ; Fit both columns into the visible area, with or without a scroll bar + Local $tRect = _WinAPI_GetClientRect($hLvPlugins) + If @error Then Return + + Local $iClientWidth = DllStructGetData($tRect, "Right") - DllStructGetData($tRect, "Left") + Local $iTypeWidth = 120 + If $iClientWidth < $iTypeWidth * 2 Then Return + + _GUICtrlListView_SetColumnWidth($hLvPlugins, 1, $iTypeWidth) + _GUICtrlListView_SetColumnWidth($hLvPlugins, 0, $iClientWidth - $iTypeWidth) +EndFunc + +Func UpdatePluginRow($iIndex) + $bPluginListUpdating = True + _GUICtrlListView_SetItemText($hLvPlugins, $iIndex, $aPluginList[$iIndex][0], 0) + _GUICtrlListView_SetItemText($hLvPlugins, $iIndex, $aPluginList[$iIndex][4], 1) + SetPluginRowState($iIndex) + $bPluginListUpdating = False +EndFunc + +Func GetSelectedPluginIndex() + If $hLvPlugins = 0 Then Return -1 + + Local $aIndices = _GUICtrlListView_GetSelectedIndices($hLvPlugins, True) + If UBound($aIndices) < 2 Or $aIndices[0] < 1 Then Return -1 + Return $aIndices[1] +EndFunc + +Func CanMovePlugin($iIndex, $iTarget) + If $bPluginListReadOnly Then Return False + If $iIndex < 0 Or $iIndex >= $iPluginListCount Then Return False + If $iTarget < 0 Or $iTarget >= $iPluginListCount Then Return False + ; Plugins loaded by the game itself keep their position + If $aPluginList[$iIndex][2] Or $aPluginList[$iTarget][2] Then Return False + ; Masters cannot be mixed with regular plugins + If $aPluginList[$iIndex][3] <> $aPluginList[$iTarget][3] Then Return False + Return True +EndFunc + +Func MoveSelectedPlugin($iDirection) + Local $iIndex = GetSelectedPluginIndex() + Local $iTarget = $iIndex + $iDirection + If Not CanMovePlugin($iIndex, $iTarget) Then Return + + For $c = 0 To 5 + Local $swap = $aPluginList[$iIndex][$c] + $aPluginList[$iIndex][$c] = $aPluginList[$iTarget][$c] + $aPluginList[$iTarget][$c] = $swap + Next + + UpdatePluginRow($iIndex) + UpdatePluginRow($iTarget) + + $bPluginListUpdating = True + _GUICtrlListView_SetItemSelected($hLvPlugins, $iIndex, False) + _GUICtrlListView_SetItemSelected($hLvPlugins, $iTarget, True, True) + $bPluginListUpdating = False + _GUICtrlListView_EnsureVisible($hLvPlugins, $iTarget) + + $bPluginListDirty = True + UpdatePluginButtons() +EndFunc + +Func SetAllPluginsEnabled($bEnabled) + If $bPluginListReadOnly Then Return + + For $i = 0 To $iPluginListCount - 1 + If $aPluginList[$i][2] Then ContinueLoop ; locked plugins stay enabled + If $aPluginList[$i][1] = $bEnabled Then ContinueLoop + $aPluginList[$i][1] = $bEnabled + UpdatePluginRow($i) + $bPluginListDirty = True + Next + + UpdatePluginButtons() +EndFunc + +Func UpdatePluginButtons() + Local $iIndex = GetSelectedPluginIndex() + GUICtrlSetState($btnPluginUp, CanMovePlugin($iIndex, $iIndex - 1) ? $GUI_ENABLE : $GUI_DISABLE) + GUICtrlSetState($btnPluginDown, CanMovePlugin($iIndex, $iIndex + 1) ? $GUI_ENABLE : $GUI_DISABLE) + + Local $iEnabled = 0 + Local $iChangeable = 0 + For $i = 0 To $iPluginListCount - 1 + If $aPluginList[$i][1] Then $iEnabled += 1 + If Not IsPluginRowFixed($i) Then $iChangeable += 1 + Next + + GUICtrlSetData($lblPluginsCount, $iEnabled & " of " & $iPluginListCount & " enabled") + GUICtrlSetState($btnPluginsEnableAll, $iChangeable > 0 ? $GUI_ENABLE : $GUI_DISABLE) + GUICtrlSetState($btnPluginsDisableAll, $iChangeable > 0 ? $GUI_ENABLE : $GUI_DISABLE) +EndFunc + +Func SavePluginsFile() + ; Under MO2 the load order is owned by the active profile + If $bPluginListReadOnly Then Return + If Not $bPluginListLoaded Or Not $bPluginListDirty Or $iPluginListCount = 0 Then Return + + Local $content = "" + For $i = 0 To $iPluginListCount - 1 + ; Plugins the game loads on its own are only written back when they were listed before + If $aPluginList[$i][2] And Not $aPluginList[$i][5] Then ContinueLoop + Local $bEnabled = $aPluginList[$i][2] ? True : $aPluginList[$i][1] + $content &= ($bEnabled ? "*" : "") & $aPluginList[$i][0] & @CRLF + Next + + If Not DirExists($PLUGINS_DIR) Then DirCreate($PLUGINS_DIR) + + Local $hFile = FileOpen($PLUGINS_FILE, BitOR($FO_OVERWRITE, $FO_UTF8_NOBOM)) + If $hFile = -1 Then + MsgBox(16, "Error", "Unable to write the plugin list:" & @CRLF & $PLUGINS_FILE) + Return + EndIf + + FileWrite($hFile, $content) + FileClose($hFile) + + $bPluginListDirty = False +EndFunc + ; ============================================ ; Main Loop ; ============================================ @@ -1313,6 +1987,18 @@ Func MainLoop() Case $btnOpenSavegames OpenSavegamesDirectory() + Case $btnPluginUp + MoveSelectedPlugin(-1) + + Case $btnPluginDown + MoveSelectedPlugin(1) + + Case $btnPluginsEnableAll + SetAllPluginsEnabled(True) + + Case $btnPluginsDisableAll + SetAllPluginsEnabled(False) + EndSwitch WEnd EndFunc @@ -1332,6 +2018,8 @@ Func ShowSettings() EndFunc Func HideSettings() + ; Drop unsaved plugin changes so the list is read again the next time + $bPluginListDirty = False GUISetState(@SW_HIDE, $hSettingsGUI) EndFunc @@ -1464,6 +2152,9 @@ Func LoadSettings() If @error Then $startSteam = 1 SetCheckboxState($chkStartSteam, $startSteam) EndIf + + ; Load the plugin list + LoadPluginList() EndFunc Func SaveSettings() @@ -1530,6 +2221,9 @@ Func SaveSettings() Local $startSteam = (GUICtrlRead($chkStartSteam) = $GUI_CHECKED) ? 1 : 0 RegWrite($REG_KEY, $REG_START_STEAM, "REG_DWORD", $startSteam) EndIf + + ; Save the plugin list + SavePluginsFile() EndFunc Func ApplyDetailPreset($presetName) @@ -1645,9 +2339,8 @@ Func OpenSavegamesDirectory() EndIf ; Check if running under Mod Organizer (parent process is ModOrganizer.exe) - Local $mo2Dir = GetMO2Directory() - If $mo2Dir <> "" Then - Local $explorerPPPath = $mo2Dir & "\explorer++\Explorer++.exe" + If $MO2_DIR <> "" Then + Local $explorerPPPath = $MO2_DIR & "\explorer++\Explorer++.exe" If FileExists($explorerPPPath) Then Run('"' & $explorerPPPath & '" "' & $fullSavePath & '"') Return