Added plugins.txt manager to the launcher

This commit is contained in:
Eddoursul 2026-07-30 15:15:54 +02:00
parent 734ee8888f
commit 2eaab91a27

View File

@ -10,7 +10,18 @@
#include <ButtonConstants.au3>
#include <StaticConstants.au3>
#include <ComboConstants.au3>
#include <ListViewConstants.au3>
#include <GuiListView.au3>
#include <GuiImageList.au3>
#include <FileConstants.au3>
#include <StructureConstants.au3>
#include <WindowsNotifsConstants.au3>
#include <WindowsSysColorConstants.au3>
#include <APIThemeConstants.au3>
#include <Array.au3>
#include <WinAPI.au3>
#include <WinAPITheme.au3>
#include <WinAPIGdiDC.au3>
#include <WinAPIProc.au3>
#include <SendMessage.au3>
@ -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